about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--edit.mu668
1 files changed, 360 insertions, 308 deletions
diff --git a/edit.mu b/edit.mu
index a3235e79..b4e43541 100644
--- a/edit.mu
+++ b/edit.mu
@@ -556,7 +556,7 @@ recipe editor-event-loop [
       jump +continue:label
     }
     # other events - send to appropriate editor
-    handle-event screen, console, editor, e
+    handle-keyboard-event screen, console, editor, e
     +continue
     row:number, screen <- render screen, editor
     # clear next line, in case we just processed a backspace
@@ -569,7 +569,7 @@ recipe editor-event-loop [
   }
 ]
 
-recipe handle-event [
+recipe handle-keyboard-event [
   local-scope
   screen:address <- next-ingredient
   console:address <- next-ingredient
@@ -580,55 +580,13 @@ recipe handle-event [
   {
     c:address:character <- maybe-convert e, text:variant
     break-unless c
-    ## check for special characters
-    # backspace - delete character before cursor
-    {
-      backspace?:boolean <- equal *c, 8/backspace
-      break-unless backspace?
-      delete-before-cursor editor
-      reply
-    }
-    # ctrl-a - move cursor to start of line
-    {
-      ctrl-a?:boolean <- equal *c, 1/ctrl-a
-      break-unless ctrl-a?
-      move-to-start-of-line editor
-      reply
-    }
-    # ctrl-e - move cursor to end of line
-    {
-      ctrl-e?:boolean <- equal *c, 5/ctrl-e
-      break-unless ctrl-e?
-      move-to-end-of-line editor
-      reply
-    }
-    # ctrl-u - delete until start of line (excluding cursor)
-    {
-      ctrl-u?:boolean <- equal *c, 21/ctrl-u
-      break-unless ctrl-u?
-      delete-to-start-of-line editor
-      reply
-    }
-    # ctrl-k - delete until end of line (including cursor)
-    {
-      ctrl-k?:boolean <- equal *c, 11/ctrl-k
-      break-unless ctrl-k?
-      delete-to-end-of-line editor
-      reply
-    }
-    # tab - insert two spaces
-    {
-      tab?:boolean <- equal *c, 9/tab
-      break-unless tab?
-      insert-at-cursor editor, 32/space, screen
-      insert-at-cursor editor, 32/space, screen
-      reply
-    }
+    # exceptions for special characters go here
+    +handle-special-character
     # otherwise type it in
     insert-at-cursor editor, *c, screen
     reply
   }
-  # otherwise it's a special key
+  # special key
   k:address:number <- maybe-convert e:event, keycode:variant
   assert k, [event was of unknown type; neither keyboard nor mouse]
   d:address:duplex-list <- get *editor, data:offset
@@ -638,107 +596,8 @@ recipe handle-event [
   screen-height:number <- screen-height screen
   left:number <- get *editor, left:offset
   right:number <- get *editor, right:offset
-  # arrows; update cursor-row and cursor-column, leave before-cursor to 'render'.
-  # right arrow
-  {
-    move-to-next-character?:boolean <- equal *k, 65514/right-arrow
-    break-unless move-to-next-character?
-    # if not at end of text
-    old-cursor:address:duplex-list <- next-duplex *before-cursor
-    break-unless old-cursor
-    # scan to next character
-    *before-cursor <- copy old-cursor
-    # if crossed a newline, move cursor to start of next row
-    {
-      old-cursor-character:character <- get **before-cursor, value:offset
-      was-at-newline?:boolean <- equal old-cursor-character, 10/newline
-      break-unless was-at-newline?
-      *cursor-row <- add *cursor-row, 1
-      *cursor-column <- copy left
-      # todo: what happens when cursor is too far down?
-      screen-height <- screen-height screen
-      above-screen-bottom?:boolean <- lesser-than *cursor-row, screen-height
-      assert above-screen-bottom?, [unimplemented: moving past bottom of screen]
-      reply
-    }
-    # if the line wraps, move cursor to start of next row
-    {
-      # if we're at the column just before the wrap indicator
-      wrap-column:number <- subtract right, 1
-      at-wrap?:boolean <- equal *cursor-column, wrap-column
-      break-unless at-wrap?
-      # and if next character isn't newline
-      new-cursor:address:duplex-list <- next-duplex old-cursor
-      break-unless new-cursor
-      next-character:character <- get *new-cursor, value:offset
-      newline?:boolean <- equal next-character, 10/newline
-      break-if newline?
-      *cursor-row <- add *cursor-row, 1
-      *cursor-column <- copy left
-      # todo: what happens when cursor is too far down?
-      above-screen-bottom?:boolean <- lesser-than *cursor-row, screen-height
-      assert above-screen-bottom?, [unimplemented: moving past bottom of screen]
-      reply
-    }
-    # otherwise move cursor one character right
-    *cursor-column <- add *cursor-column, 1
-  }
-  # left arrow
-  {
-    move-to-previous-character?:boolean <- equal *k, 65515/left-arrow
-    break-unless move-to-previous-character?
-#?     trace [app], [left arrow] #? 1
-    # if not at start of text (before-cursor at § sentinel)
-    prev:address:duplex-list <- prev-duplex *before-cursor
-    break-unless prev
-    editor <- move-cursor-coordinates-left editor
-  }
-  # down arrow
-  {
-    move-to-next-line?:boolean <- equal *k, 65516/down-arrow
-    break-unless move-to-next-line?
-    # todo: support scrolling
-    already-at-bottom?:boolean <- greater-or-equal *cursor-row, screen-height
-    break-if already-at-bottom?
-#?     $print [moving down
-#? ] #? 1
-    *cursor-row <- add *cursor-row, 1
-    # that's it; render will adjust cursor-column as necessary
-  }
-  # up arrow
-  {
-    move-to-previous-line?:boolean <- equal *k, 65517/up-arrow
-    break-unless move-to-previous-line?
-    # todo: support scrolling
-    already-at-top?:boolean <- lesser-or-equal *cursor-row, 1/top
-    break-if already-at-top?
-#?     $print [moving up
-#? ] #? 1
-    *cursor-row <- subtract *cursor-row, 1
-    # that's it; render will adjust cursor-column as necessary
-  }
-  # home
-  {
-    home?:boolean <- equal *k, 65521/home
-    break-unless home?
-    move-to-start-of-line editor
-    reply
-  }
-  # end
-  {
-    end?:boolean <- equal *k, 65520/end
-    break-unless end?
-    move-to-end-of-line editor
-    reply
-  }
-  # delete
-  {
-    delete?:boolean <- equal *k, 65522/delete
-    break-unless delete?
-    curr:address:duplex-list <- get **before-cursor, next:offset
-    _ <- remove-duplex curr
-    reply
-  }
+  # handlers for each special key will go here
+  +handle-special-key
 ]
 
 # process click, return if it was on current editor
@@ -824,18 +683,6 @@ recipe insert-at-cursor [
   *cursor-column <- add *cursor-column, 1
 ]
 
-recipe delete-before-cursor [
-  local-scope
-  editor:address:editor-data <- next-ingredient
-  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
-  # if at start of text (before-cursor at § sentinel), return
-  prev:address:duplex-list <- prev-duplex *before-cursor
-  reply-unless prev
-  editor <- move-cursor-coordinates-left editor
-  remove-duplex *before-cursor
-  *before-cursor <- copy prev
-]
-
 recipe move-cursor-coordinates-left [
   local-scope
   editor:address:editor-data <- next-ingredient
@@ -930,104 +777,6 @@ recipe line-indent [
   reply result
 ]
 
-recipe move-to-start-of-line [
-  local-scope
-  editor:address:editor-data <- next-ingredient
-  # update cursor column
-  left:number <- get *editor, left:offset
-  cursor-column:address:number <- get-address *editor, cursor-column:offset
-  *cursor-column <- copy left
-  # update before-cursor
-  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
-  init:address:duplex-list <- get *editor, data:offset
-  # while not at start of line, move 
-  {
-    at-start-of-text?:boolean <- equal *before-cursor, init
-    break-if at-start-of-text?
-    prev:character <- get **before-cursor, value:offset
-    at-start-of-line?:boolean <- equal prev, 10/newline
-    break-if at-start-of-line?
-    *before-cursor <- prev-duplex *before-cursor
-    assert *before-cursor, [move-to-start-of-line tried to move before start of text]
-    loop
-  }
-]
-
-recipe move-to-end-of-line [
-  local-scope
-  editor:address:editor-data <- next-ingredient
-  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
-  cursor-column:address:number <- get-address *editor, cursor-column:offset
-  # while not at start of line, move 
-  {
-    next:address:duplex-list <- next-duplex *before-cursor
-    break-unless next  # end of text
-    nextc:character <- get *next, value:offset
-    at-end-of-line?:boolean <- equal nextc, 10/newline
-    break-if at-end-of-line?
-    *before-cursor <- copy next
-    *cursor-column <- add *cursor-column, 1
-    loop
-  }
-  # move one past final character
-  *cursor-column <- add *cursor-column, 1
-]
-
-recipe delete-to-start-of-line [
-  local-scope
-  editor:address:editor-data <- next-ingredient
-  # compute range to delete
-  init:address:duplex-list <- get *editor, data:offset
-  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
-  start:address:duplex-list <- copy *before-cursor
-  end:address:duplex-list <- next-duplex *before-cursor
-  {
-    at-start-of-text?:boolean <- equal start, init
-    break-if at-start-of-text?
-    curr:character <- get *start, value:offset
-    at-start-of-line?:boolean <- equal curr, 10/newline
-    break-if at-start-of-line?
-    start <- prev-duplex start
-    assert start, [delete-to-start-of-line tried to move before start of text]
-    loop
-  }
-  # snip it out
-  start-next:address:address:duplex-list <- get-address *start, next:offset
-  *start-next <- copy end
-  end-prev:address:address:duplex-list <- get-address *end, prev:offset
-  *end-prev <- copy start
-  # adjust cursor
-  *before-cursor <- prev-duplex end
-  left:number <- get *editor, left:offset
-  cursor-column:address:number <- get-address *editor, cursor-column:offset
-  *cursor-column <- copy left
-]
-
-recipe delete-to-end-of-line [
-  local-scope
-  editor:address:editor-data <- next-ingredient
-  # compute range to delete
-  start:address:duplex-list <- get *editor, before-cursor:offset
-  end:address:duplex-list <- next-duplex start
-  {
-    at-end-of-text?:boolean <- equal end, 0/null
-    break-if at-end-of-text?
-    curr:character <- get *end, value:offset
-    at-end-of-line?:boolean <- equal curr, 10/newline
-    break-if at-end-of-line?
-    end <- next-duplex end
-    loop
-  }
-  # snip it out
-  start-next:address:address:duplex-list <- get-address *start, next:offset
-  *start-next <- copy end
-  {
-    break-unless end
-    end-prev:address:address:duplex-list <- get-address *end, prev:offset
-    *end-prev <- copy start
-  }
-]
-
 scenario editor-handles-empty-event-queue [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [abc]
@@ -1451,34 +1200,45 @@ ef]
   ]
 ]
 
-scenario editor-handles-delete-key [
+## 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 [
   assume-screen 10/width, 5/height
-  1:address:array:character <- new [abc]
-  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right
-  assume-console [
-    press 65522  # delete
-  ]
-  run [
-    editor-event-loop screen:address, console:address, 2:address:editor-data
-  ]
-  screen-should-contain [
-    .          .
-    .bc        .
-    .          .
-  ]
+  # just one character in final line
+  1:address:array:character <- new [ab
+cd]
+  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right
   assume-console [
-    press 65522  # delete
+    type [»]
   ]
+  3:event/tab <- merge 0/text, 9/tab, 0/dummy, 0/dummy
+  replace-in-console 187/», 3:event/tab
   run [
     editor-event-loop screen:address, console:address, 2:address:editor-data
   ]
   screen-should-contain [
     .          .
-    .c         .
-    .          .
+    .  ab      .
+    .cd        .
   ]
 ]
 
+after +handle-special-character [
+  {
+    tab?:boolean <- equal *c, 9/tab
+    break-unless tab?
+    insert-at-cursor editor, 32/space, screen
+    insert-at-cursor editor, 32/space, screen
+    reply
+  }
+]
+
+# backspace - delete character before cursor
+
 scenario editor-handles-backspace-key [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [abc]
@@ -1505,6 +1265,27 @@ scenario editor-handles-backspace-key [
   ]
 ]
 
+after +handle-special-character [
+  {
+    backspace?:boolean <- equal *c, 8/backspace
+    break-unless backspace?
+    delete-before-cursor editor
+    reply
+  }
+]
+
+recipe delete-before-cursor [
+  local-scope
+  editor:address:editor-data <- next-ingredient
+  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
+  # if at start of text (before-cursor at § sentinel), return
+  prev:address:duplex-list <- prev-duplex *before-cursor
+  reply-unless prev
+  editor <- move-cursor-coordinates-left editor
+  remove-duplex *before-cursor
+  *before-cursor <- copy prev
+]
+
 scenario editor-clears-last-line-on-backspace [
   assume-screen 10/width, 5/height
   # just one character in final line
@@ -1533,27 +1314,48 @@ cd]
   ]
 ]
 
-scenario editor-inserts-two-spaces-on-tab [
+# delete - delete character at cursor
+
+scenario editor-handles-delete-key [
   assume-screen 10/width, 5/height
-  # just one character in final line
-  1:address:array:character <- new [ab
-cd]
-  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right
+  1:address:array:character <- new [abc]
+  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right
   assume-console [
-    type [»]
+    press 65522  # delete
   ]
-  3:event/tab <- merge 0/text, 9/tab, 0/dummy, 0/dummy
-  replace-in-console 187/», 3:event/tab
   run [
     editor-event-loop screen:address, console:address, 2:address:editor-data
   ]
   screen-should-contain [
     .          .
-    .  ab      .
-    .cd        .
+    .bc        .
+    .          .
+  ]
+  assume-console [
+    press 65522  # delete
+  ]
+  run [
+    editor-event-loop screen:address, console:address, 2:address:editor-data
   ]
+  screen-should-contain [
+    .          .
+    .c         .
+    .          .
+  ]
+]
+
+after +handle-special-key [
+  {
+    delete?:boolean <- equal *k, 65522/delete
+    break-unless delete?
+    curr:address:duplex-list <- get **before-cursor, next:offset
+    _ <- remove-duplex curr
+    reply
+  }
 ]
 
+# right arrow
+
 scenario editor-moves-cursor-right-with-key [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [abc]
@@ -1572,6 +1374,52 @@ scenario editor-moves-cursor-right-with-key [
   ]
 ]
 
+after +handle-special-key [
+  {
+    move-to-next-character?:boolean <- equal *k, 65514/right-arrow
+    break-unless move-to-next-character?
+    # if not at end of text
+    old-cursor:address:duplex-list <- next-duplex *before-cursor
+    break-unless old-cursor
+    # scan to next character
+    *before-cursor <- copy old-cursor
+    # if crossed a newline, move cursor to start of next row
+    {
+      old-cursor-character:character <- get **before-cursor, value:offset
+      was-at-newline?:boolean <- equal old-cursor-character, 10/newline
+      break-unless was-at-newline?
+      *cursor-row <- add *cursor-row, 1
+      *cursor-column <- copy left
+      # todo: what happens when cursor is too far down?
+      screen-height <- screen-height screen
+      above-screen-bottom?:boolean <- lesser-than *cursor-row, screen-height
+      assert above-screen-bottom?, [unimplemented: moving past bottom of screen]
+      reply
+    }
+    # if the line wraps, move cursor to start of next row
+    {
+      # if we're at the column just before the wrap indicator
+      wrap-column:number <- subtract right, 1
+      at-wrap?:boolean <- equal *cursor-column, wrap-column
+      break-unless at-wrap?
+      # and if next character isn't newline
+      new-cursor:address:duplex-list <- next-duplex old-cursor
+      break-unless new-cursor
+      next-character:character <- get *new-cursor, value:offset
+      newline?:boolean <- equal next-character, 10/newline
+      break-if newline?
+      *cursor-row <- add *cursor-row, 1
+      *cursor-column <- copy left
+      # todo: what happens when cursor is too far down?
+      above-screen-bottom?:boolean <- lesser-than *cursor-row, screen-height
+      assert above-screen-bottom?, [unimplemented: moving past bottom of screen]
+      reply
+    }
+    # otherwise move cursor one character right
+    *cursor-column <- add *cursor-column, 1
+  }
+]
+
 scenario editor-moves-cursor-to-next-line-with-right-arrow [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [abc
@@ -1723,6 +1571,8 @@ d]
   ]
 ]
 
+# left arrow
+
 scenario editor-moves-cursor-left-with-key [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [abc]
@@ -1742,6 +1592,18 @@ scenario editor-moves-cursor-left-with-key [
   ]
 ]
 
+after +handle-special-key [
+  {
+    move-to-previous-character?:boolean <- equal *k, 65515/left-arrow
+    break-unless move-to-previous-character?
+#?     trace [app], [left arrow] #? 1
+    # if not at start of text (before-cursor at § sentinel)
+    prev:address:duplex-list <- prev-duplex *before-cursor
+    break-unless prev
+    editor <- move-cursor-coordinates-left editor
+  }
+]
+
 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [
   assume-screen 10/width, 5/height
   # initialize editor with two lines
@@ -1866,6 +1728,8 @@ scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [
   ]
 ]
 
+# up arrow
+
 scenario editor-moves-to-previous-line-with-up-arrow [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [abc
@@ -1886,13 +1750,25 @@ def]
   ]
 ]
 
-scenario editor-moves-to-next-line-with-down-arrow [
+after +handle-special-key [
+  {
+    move-to-previous-line?:boolean <- equal *k, 65517/up-arrow
+    break-unless move-to-previous-line?
+    # todo: support scrolling
+    already-at-top?:boolean <- lesser-or-equal *cursor-row, 1/top
+    break-if already-at-top?
+    *cursor-row <- subtract *cursor-row, 1
+    # that's it; render will adjust cursor-column as necessary
+  }
+]
+
+scenario editor-adjusts-column-at-next-line [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [abc
-def]
+de]
   2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right
-  # cursor starts out at (1, 0)
   assume-console [
+    left-click 1, 3
     press 65516  # down arrow
   ]
   run [
@@ -1900,41 +1776,57 @@ def]
     3:number <- get *2:address:editor-data, cursor-row:offset
     4:number <- get *2:address:editor-data, cursor-column:offset
   ]
-  # ..and ends at (2, 0)
   memory-should-contain [
     3 <- 2
-    4 <- 0
+    4 <- 2
   ]
 ]
 
-scenario editor-adjusts-column-at-previous-line [
+# down arrow
+
+scenario editor-moves-to-next-line-with-down-arrow [
   assume-screen 10/width, 5/height
-  1:address:array:character <- new [ab
+  1:address:array:character <- new [abc
 def]
   2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right
+  # cursor starts out at (1, 0)
   assume-console [
-    left-click 2, 3
-    press 65517  # up arrow
+    press 65516  # down arrow
   ]
   run [
     editor-event-loop screen:address, console:address, 2:address:editor-data
     3:number <- get *2:address:editor-data, cursor-row:offset
     4:number <- get *2:address:editor-data, cursor-column:offset
   ]
+  # ..and ends at (2, 0)
   memory-should-contain [
-    3 <- 1
-    4 <- 2
+    3 <- 2
+    4 <- 0
   ]
 ]
 
-scenario editor-adjusts-column-at-next-line [
+after +handle-special-key [
+  {
+    move-to-next-line?:boolean <- equal *k, 65516/down-arrow
+    break-unless move-to-next-line?
+    # todo: support scrolling
+    already-at-bottom?:boolean <- greater-or-equal *cursor-row, screen-height
+    break-if already-at-bottom?
+#?     $print [moving down
+#? ] #? 1
+    *cursor-row <- add *cursor-row, 1
+    # that's it; render will adjust cursor-column as necessary
+  }
+]
+
+scenario editor-adjusts-column-at-previous-line [
   assume-screen 10/width, 5/height
-  1:address:array:character <- new [abc
-de]
+  1:address:array:character <- new [ab
+def]
   2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right
   assume-console [
-    left-click 1, 3
-    press 65516  # down arrow
+    left-click 2, 3
+    press 65517  # up arrow
   ]
   run [
     editor-event-loop screen:address, console:address, 2:address:editor-data
@@ -1942,11 +1834,13 @@ de]
     4:number <- get *2:address:editor-data, cursor-column:offset
   ]
   memory-should-contain [
-    3 <- 2
+    3 <- 1
     4 <- 2
   ]
 ]
 
+# ctrl-a/home - move cursor to start of line
+
 scenario editor-moves-to-start-of-line-with-ctrl-a [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -1971,6 +1865,47 @@ scenario editor-moves-to-start-of-line-with-ctrl-a [
   ]
 ]
 
+after +handle-special-character [
+  {
+    ctrl-a?:boolean <- equal *c, 1/ctrl-a
+    break-unless ctrl-a?
+    move-to-start-of-line editor
+    reply
+  }
+]
+
+after +handle-special-key [
+  {
+    home?:boolean <- equal *k, 65521/home
+    break-unless home?
+    move-to-start-of-line editor
+    reply
+  }
+]
+
+recipe move-to-start-of-line [
+  local-scope
+  editor:address:editor-data <- next-ingredient
+  # update cursor column
+  left:number <- get *editor, left:offset
+  cursor-column:address:number <- get-address *editor, cursor-column:offset
+  *cursor-column <- copy left
+  # update before-cursor
+  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
+  init:address:duplex-list <- get *editor, data:offset
+  # while not at start of line, move 
+  {
+    at-start-of-text?:boolean <- equal *before-cursor, init
+    break-if at-start-of-text?
+    prev:character <- get **before-cursor, value:offset
+    at-start-of-line?:boolean <- equal prev, 10/newline
+    break-if at-start-of-line?
+    *before-cursor <- prev-duplex *before-cursor
+    assert *before-cursor, [move-to-start-of-line tried to move before start of text]
+    loop
+  }
+]
+
 scenario editor-moves-to-start-of-line-with-ctrl-a-2 [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -2039,6 +1974,8 @@ scenario editor-moves-to-start-of-line-with-home-2 [
   ]
 ]
 
+# ctrl-e/end - move cursor to end of line
+
 scenario editor-moves-to-start-of-line-with-ctrl-e [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -2082,6 +2019,44 @@ scenario editor-moves-to-start-of-line-with-ctrl-e [
   ]
 ]
 
+after +handle-special-character [
+  {
+    ctrl-e?:boolean <- equal *c, 5/ctrl-e
+    break-unless ctrl-e?
+    move-to-end-of-line editor
+    reply
+  }
+]
+
+after +handle-special-key [
+  {
+    end?:boolean <- equal *k, 65520/end
+    break-unless end?
+    move-to-end-of-line editor
+    reply
+  }
+]
+
+recipe move-to-end-of-line [
+  local-scope
+  editor:address:editor-data <- next-ingredient
+  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
+  cursor-column:address:number <- get-address *editor, cursor-column:offset
+  # while not at start of line, move 
+  {
+    next:address:duplex-list <- next-duplex *before-cursor
+    break-unless next  # end of text
+    nextc:character <- get *next, value:offset
+    at-end-of-line?:boolean <- equal nextc, 10/newline
+    break-if at-end-of-line?
+    *before-cursor <- copy next
+    *cursor-column <- add *cursor-column, 1
+    loop
+  }
+  # move one past final character
+  *cursor-column <- add *cursor-column, 1
+]
+
 scenario editor-moves-to-end-of-line-with-ctrl-e-2 [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -2150,6 +2125,8 @@ scenario editor-moves-to-end-of-line-with-end-2 [
   ]
 ]
 
+# ctrl-u - delete text from start of line until (but not at) cursor
+
 scenario editor-deletes-to-start-of-line-with-ctrl-u [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -2174,6 +2151,45 @@ scenario editor-deletes-to-start-of-line-with-ctrl-u [
   ]
 ]
 
+after +handle-special-character [
+  {
+    ctrl-u?:boolean <- equal *c, 21/ctrl-u
+    break-unless ctrl-u?
+    delete-to-start-of-line editor
+    reply
+  }
+]
+
+recipe delete-to-start-of-line [
+  local-scope
+  editor:address:editor-data <- next-ingredient
+  # compute range to delete
+  init:address:duplex-list <- get *editor, data:offset
+  before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset
+  start:address:duplex-list <- copy *before-cursor
+  end:address:duplex-list <- next-duplex *before-cursor
+  {
+    at-start-of-text?:boolean <- equal start, init
+    break-if at-start-of-text?
+    curr:character <- get *start, value:offset
+    at-start-of-line?:boolean <- equal curr, 10/newline
+    break-if at-start-of-line?
+    start <- prev-duplex start
+    assert start, [delete-to-start-of-line tried to move before start of text]
+    loop
+  }
+  # snip it out
+  start-next:address:address:duplex-list <- get-address *start, next:offset
+  *start-next <- copy end
+  end-prev:address:address:duplex-list <- get-address *end, prev:offset
+  *end-prev <- copy start
+  # adjust cursor
+  *before-cursor <- prev-duplex end
+  left:number <- get *editor, left:offset
+  cursor-column:address:number <- get-address *editor, cursor-column:offset
+  *cursor-column <- copy left
+]
+
 scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -2184,8 +2200,8 @@ scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
     left-click 1, 2
     type [u]  # ctrl-u
   ]
-  3:event/ctrl-u <- merge 0/text, 21/ctrl-a, 0/dummy, 0/dummy
-  replace-in-console 117/a, 3:event/ctrl-u
+  3:event/ctrl-u <- merge 0/text, 21/ctrl-u, 0/dummy, 0/dummy
+  replace-in-console 117/u, 3:event/ctrl-u
   run [
     editor-event-loop screen:address, console:address, 2:address:editor-data
   ]
@@ -2208,8 +2224,8 @@ scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
     left-click 1, 3
     type [u]  # ctrl-u
   ]
-  3:event/ctrl-u <- merge 0/text, 21/ctrl-a, 0/dummy, 0/dummy
-  replace-in-console 117/a, 3:event/ctrl-u
+  3:event/ctrl-u <- merge 0/text, 21/ctrl-u, 0/dummy, 0/dummy
+  replace-in-console 117/u, 3:event/ctrl-u
   run [
     editor-event-loop screen:address, console:address, 2:address:editor-data
   ]
@@ -2222,6 +2238,8 @@ scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
   ]
 ]
 
+# ctrl-k - delete text from cursor to end of line (but not the newline)
+
 scenario editor-deletes-to-end-of-line-with-ctrl-k [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -2246,6 +2264,40 @@ scenario editor-deletes-to-end-of-line-with-ctrl-k [
   ]
 ]
 
+after +handle-special-character [
+  {
+    ctrl-k?:boolean <- equal *c, 11/ctrl-k
+    break-unless ctrl-k?
+    delete-to-end-of-line editor
+    reply
+  }
+]
+
+recipe delete-to-end-of-line [
+  local-scope
+  editor:address:editor-data <- next-ingredient
+  # compute range to delete
+  start:address:duplex-list <- get *editor, before-cursor:offset
+  end:address:duplex-list <- next-duplex start
+  {
+    at-end-of-text?:boolean <- equal end, 0/null
+    break-if at-end-of-text?
+    curr:character <- get *end, value:offset
+    at-end-of-line?:boolean <- equal curr, 10/newline
+    break-if at-end-of-line?
+    end <- next-duplex end
+    loop
+  }
+  # snip it out
+  start-next:address:address:duplex-list <- get-address *start, next:offset
+  *start-next <- copy end
+  {
+    break-unless end
+    end-prev:address:address:duplex-list <- get-address *end, prev:offset
+    *end-prev <- copy start
+  }
+]
+
 scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [
   assume-screen 10/width, 5/height
   1:address:array:character <- new [123
@@ -2463,11 +2515,11 @@ recipe event-loop [
     {
       {
         break-if *sandbox-in-focus?
-        handle-event screen, console, recipes, e:event
+        handle-keyboard-event screen, console, recipes, e:event
       }
       {
         break-unless *sandbox-in-focus?
-        handle-event screen, console, current-sandbox, e:event
+        handle-keyboard-event screen, console, current-sandbox, e:event
       }
       # optimization: refresh screen only if no more events
       # todo: test this