# Environment for learning programming using mu. recipe main [ local-scope open-console initial-recipe:address:array:character <- restore [recipes.mu] initial-sandbox:address:array:character <- new [] env:address:programming-environment-data <- new-programming-environment 0/screen, initial-recipe:address:array:character, initial-sandbox:address:array:character env:address:programming-environment-data <- restore-sandboxes env:address:programming-environment-data render-all 0/screen, env:address:programming-environment-data show-screen 0/screen event-loop 0/screen, 0/console, env:address:programming-environment-data # never gets here ] container programming-environment-data [ recipes:address:editor-data recipe-warnings:address:array:character current-sandbox:address:editor-data sandbox:address:sandbox-data sandbox-in-focus?:boolean # false => focus in recipes; true => focus in current-sandbox ] recipe new-programming-environment [ local-scope screen:address <- next-ingredient initial-recipe-contents:address:array:character <- next-ingredient initial-sandbox-contents:address:array:character <- next-ingredient width:number <- screen-width screen:address height:number <- screen-height screen:address # top menu result:address:programming-environment-data <- new programming-environment-data:type draw-horizontal screen:address, 0, 0/left, width:number, 32/space, 0/black, 238/grey button-start:number <- subtract width:number, 20 button-on-screen?:boolean <- greater-or-equal button-start:number, 0 assert button-on-screen?:boolean, [screen too narrow for menu] move-cursor screen:address, 0/row, button-start:number/column run-button:address:array:character <- new [ run (F4) ] print-string screen:address, run-button:address:array:character, 255/white, 161/reddish # dotted line down the middle divider:number, _ <- divide-with-remainder width:number, 2 draw-vertical screen:address, divider:number, 1/top, height:number, 9482/vertical-dotted # recipe editor on the left recipes:address:address:editor-data <- get-address result:address:programming-environment-data/lookup, recipes:offset recipes:address:address:editor-data/lookup <- new-editor initial-recipe-contents:address:array:character, screen:address, 0/left, divider:number/right # sandbox editor on the right new-left:number <- add divider:number, 1 new-right:number <- add new-left:number, 5 current-sandbox:address:address:editor-data <- get-address result:address:programming-environment-data/lookup, current-sandbox:offset current-sandbox:address:address:editor-data/lookup <- new-editor initial-sandbox-contents:address:array:character, screen:address, new-left:number, width:number screen:address <- render-all screen:address, result:address:programming-environment-data reply result:address:programming-environment-data ] scenario editor-initially-prints-string-to-screen [ assume-screen 10/width, 5/height run [ 1:address:array:character <- new [abc] new-editor 1:address:array:character, screen:address, 0/left, 10/right ] screen-should-contain [ . . .abc . . . ] ] ## In which we introduce the editor data structure, and show how it displays ## text to the screen. container editor-data [ # editable text: doubly linked list of characters (head contains a special sentinel) data:address:duplex-list # location before cursor inside data before-cursor:address:duplex-list # raw bounds of display area on screen # always displays from row 1 and at most until bottom of screen left:number right:number # raw screen coordinates of cursor cursor-row:number cursor-column:number ] # editor:address, screen:address <- new-editor s:address:array:character, screen:address, left:number, right:number # creates a new editor widget and renders its initial appearance to screen. # top/left/right constrain the screen area available to the new editor. # right is exclusive. recipe new-editor [ local-scope s:address:array:character <- next-ingredient screen:address <- next-ingredient # no clipping of bounds left:number <- next-ingredient right:number <- next-ingredient right:number <- subtract right:number, 1 result:address:editor-data <- new editor-data:type # initialize screen-related fields x:address:number <- get-address result:address:editor-data/lookup, left:offset x:address:number/lookup <- copy left:number x:address:number <- get-address result:address:editor-data/lookup, right:offset x:address:number/lookup <- copy right:number # initialize cursor x:address:number <- get-address result:address:editor-data/lookup, cursor-row:offset x:address:number/lookup <- copy 1/top x:address:number <- get-address result:address:editor-data/lookup, cursor-column:offset x:address:number/lookup <- copy left:number init:address:address:duplex-list <- get-address result:address:editor-data/lookup, data:offset init:address:address:duplex-list/lookup <- push-duplex 167/§, 0/tail y:address:address:duplex-list <- get-address result:address:editor-data/lookup, before-cursor:offset y:address:address:duplex-list/lookup <- copy init:address:address:duplex-list/lookup # early exit if s is empty reply-unless s:address:array:character, result:address:editor-data len:number <- length s:address:array:character/lookup reply-unless len:number, result:address:editor-data idx:number <- copy 0 # now we can start appending the rest, character by character curr:address:duplex-list <- copy init:address:address:duplex-list/lookup { done?:boolean <- greater-or-equal idx:number, len:number break-if done?:boolean c:character <- index s:address:array:character/lookup, idx:number insert-duplex c:character, curr:address:duplex-list # next iter curr:address:duplex-list <- next-duplex curr:address:duplex-list idx:number <- add idx:number, 1 loop } # initialize cursor to top of screen y:address:address:duplex-list <- get-address result:address:editor-data/lookup, before-cursor:offset y:address:address:duplex-list/lookup <- copy init:address:address:duplex-list/lookup # initial render to screen, just for some old tests _, screen:address <- render screen:address, result:address:editor-data reply result:address:editor-data ] scenario editor-initializes-without-data [ assume-screen 5/width, 3/height run [ 1:address:editor-data <- new-editor 0/data, screen:address, 2/left, 5/right 2:editor-data <- copy 1:address:editor-data/lookup ] memory-should-contain [ # 2 (data) <- just the § sentinel # 3 (before cursor) <- the § sentinel 4 <- 2 # left 5 <- 4 # right (inclusive) 6 <- 1 # cursor row 7 <- 2 # cursor column ] screen-should-contain [ . . . . . . ] ] # bottom:number, screen:address <- render screen:address, editor:address:editor-data recipe render [ local-scope screen:address <- next-ingredient editor:address:editor-data <- next-ingredient reply-unless editor:address:editor-data, 1/top, screen:address/same-as-ingredient:0 left:number <- get editor:address:editor-data/lookup, left:offset screen-height:number <- screen-height screen:address right:number <- get editor:address:editor-data/lookup, right:offset hide-screen screen:address # highlight mu code with color color:number <- copy 7/white highlighting-state:number <- copy 0/normal # traversing editor curr:address:duplex-list <- get editor:address:editor-data/lookup, data:offset prev:address:duplex-list <- copy curr:address:duplex-list curr:address:duplex-list <- next-duplex curr:address:duplex-list # traversing screen row:number <- copy 1/top column:number <- copy left:number cursor-row:address:number <- get-address editor:address:editor-data/lookup, cursor-row:offset cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/lookup, before-cursor:offset move-cursor screen:address, row:number, column:number { +next-character break-unless curr:address:duplex-list off-screen?:boolean <- greater-or-equal row:number, screen-height:number break-if off-screen?:boolean # update editor-data.before-cursor # Doing so at the start of each iteration ensures it stays one step behind # the current character. { at-cursor-row?:boolean <- equal row:number, cursor-row:address:number/lookup break-unless at-cursor-row?:boolean at-cursor?:boolean <- equal column:number, cursor-column:address:number/lookup break-unless at-cursor?:boolean before-cursor:address:address:duplex-list/lookup <- prev-duplex curr:address:duplex-list } c:character <- get curr:address:duplex-list/lookup, value:offset color:number, highlighting-state:number <- get-color color:number, highlighting-state:number, c:character { # newline? move to left rather than 0 newline?:boolean <- equal c:character, 10/newline break-unless newline?:boolean # adjust cursor if necessary { at-cursor-row?:boolean <- equal row:number, cursor-row:address:number/lookup break-unless at-cursor-row?:boolean left-of-cursor?:boolean <- lesser-than column:number, cursor-column:address:number/lookup break-unless left-of-cursor?:boolean cursor-column:address:number/lookup <- copy column:number before-cursor:address:address:duplex-list/lookup <- prev-duplex curr:address:duplex-list } # clear rest of line in this window clear-line-delimited screen:address, column:number, right:number # skip to next line row:number <- add row:number, 1 column:number <- copy left:number move-cursor screen:address, row:number, column:number curr:address:duplex-list <- next-duplex curr:address:duplex-list prev:address:duplex-list <- next-duplex prev:address:duplex-list loop +next-character:label } { # at right? wrap. even if there's only one more letter left; we need # room for clicking on the cursor after it. at-right?:boolean <- equal column:number, right:number break-unless at-right?:boolean # print wrap icon print-character screen:address, 8617/loop-back-to-left, 245/grey column:number <- copy left:number row:number <- add row:number, 1 move-cursor screen:address, row:number, column:number # don't increment curr loop +next-character:label } print-character screen:address, c:character, color:number curr:address:duplex-list <- next-duplex curr:address:duplex-list prev:address:duplex-list <- next-duplex prev:address:duplex-list column:number <- add column:number, 1 loop } # is cursor to the right of the last line? move to end { at-cursor-row?:boolean <- equal row:number, cursor-row:address:number/lookup cursor-outside-line?:boolean <- lesser-or-equal column:number, cursor-column:address:number/lookup before-cursor-on-same-line?:boolean <- and at-cursor-row?:boolean, cursor-outside-line?:boolean above-cursor-row?:boolean <- lesser-than row:number, cursor-row:address:number/lookup before-cursor?:boolean <- or before-cursor-on-same-line?:boolean, above-cursor-row?:boolean break-unless before-cursor?:boolean cursor-row:address:number/lookup <- copy row:number cursor-column:address:number/lookup <- copy column:number # line not wrapped but cursor outside bounds? wrap cursor { too-far-right?:boolean <- greater-than cursor-column:address:number/lookup, right:number break-unless too-far-right?:boolean cursor-column:address:number/lookup <- copy left:number cursor-row:address:number/lookup <- add cursor-row:address:number/lookup, 1 above-screen-bottom?:boolean <- lesser-than cursor-row:address:number/lookup, screen-height:number assert above-screen-bottom?:boolean, [unimplemented: wrapping cursor past bottom of screen] } before-cursor:address:address:duplex-list/lookup <- copy prev:address:duplex-list } # clear rest of current line clear-line-delimited screen:address, column:number, right:number reply row:number, screen:address/same-as-ingredient:0 ] # row:number, screen:address <- render-string screen:address, s:address:array:character, left:number, right:number, color:number, row:number # move cursor at start of next line # print a string 's' to 'editor' in 'color' starting at 'row' # clear rest of last line, but don't move cursor to next line recipe render-string [ local-scope screen:address <- next-ingredient s:address:array:character <- next-ingredient left:number <- next-ingredient right:number <- next-ingredient color:number <- next-ingredient row:number <- next-ingredient row:number <- add row:number, 1 reply-unless s:address:array:character, row:number/same-as-ingredient:5, screen:address/same-as-ingredient:0 column:number <- copy left:number move-cursor screen:address, row:number, column:number screen-height:number <- screen-height screen:address i:number <- copy 0 len:number <- length s:address:array:character/lookup { +next-character done?:boolean <- greater-or-equal i:number, len:number break-if done?:boolean done?:boolean <- greater-or-equal row:number, screen-height:number break-if done?:boolean c:character <- index s:address:array:character/lookup, i:number { # at right? wrap. at-right?:boolean <- equal column:number, right:number break-unless at-right?:boolean # print wrap icon print-character screen:address, 8617/loop-back-to-left, 245/grey column:number <- copy left:number row:number <- add row:number, 1 move-cursor screen:address, row:number, column:number loop +next-character:label # retry i } i:number <- add i:number, 1 { # newline? move to left rather than 0 newline?:boolean <- equal c:character, 10/newline break-unless newline?:boolean # clear rest of line in this window { done?:boolean <- greater-than column:number, right:number break-if done?:boolean print-character screen:address, 32/space column:number <- add column:number, 1 loop } row:number <- add row:number, 1 column:number <- copy left:number move-cursor screen:address, row:number, column:number loop +next-character:label } print-character screen:address, c:character, color:number column:number <- add column:number, 1 loop } { # clear rest of current line line-done?:boolean <- greater-than column:number, right:number break-if line-done?:boolean print-character screen:address, 32/space column:number <- add column:number, 1 loop } reply row:number/same-as-ingredient:5, screen:address/same-as-ingredient:0 ] # row:number, screen:address <- render-screen screen:address, sandbox-screen:address, left:number, right:number, row:number # print the fake sandbox screen to 'screen' with appropriate delimiters # leave cursor at start of next line recipe render-screen [ local-scope screen:address <- next-ingredient s:address:screen <- next-ingredient left:number <- next-ingredient right:number <- next-ingredient row:number <- next-ingredient row:number <- add row:number, 1 reply-unless s:address:screen, row:number/same-as-ingredient:4, screen:address/same-as-ingredient:0 # print 'screen:' header:address:array:character <- new [screen:] row:number <- subtract row:number, 1 # compensate for render-string below row:number <- render-string screen:address, header:address:array:character, left:number, right:number, 245/grey, row:number # newline row:number <- add row:number, 1 move-cursor screen:address, row:number, left:number # start printing s column:number <- copy left:number s-width:number <- screen-width s:address:screen s-height:number <- screen-height s:address:screen buf:address:array:screen-cell <- get s:address:screen/lookup, data:offset stop-printing:number <- add left:number, s-width:number, 3 max-column:number <- min stop-printing:number, right:number i:number <- copy 0 len:number <- length buf:address:array:screen-cell/lookup screen-height:number <- screen-height screen:address { done?:boolean <- greater-or-equal i:number, len:number break-if done?:boolean done?:boolean <- greater-or-equal row:number, screen-height:number break-if done?:boolean column:number <- copy left:number move-cursor screen:address, row:number, column:number # initial leader for each row: two spaces and a '.' print-character screen:address, 32/space, 245/grey print-character screen:address, 32/space, 245/grey print-character screen:address, 46/full-stop, 245/grey column:number <- add left:number, 3 { # print row row-done?:boolean <- greater-or-equal column:number, max-column:number break-if row-done?:boolean curr:screen-cell <- index buf:address:array:screen-cell/lookup, i:number c:character <- get curr:screen-cell, contents:offset print-character screen:address, c:character, 245/grey column:number <- add column:number, 1 i:number <- add i:number, 1 loop } # print final '.' print-character screen:address, 46/full-stop, 245/grey column:number <- add column:number, 1 { # clear rest of current line line-done?:boolean <- greater-than column:number, right:number break-if line-done?:boolean print-character screen:address, 32/space column:number <- add column:number, 1 loop } row:number <- add row:number, 1 loop } reply row:number/same-as-ingredient:4, screen:address/same-as-ingredient:0 ] recipe clear-line-delimited [ local-scope screen:address <- next-ingredient left:number <- next-ingredient right:number <- next-ingredient column:number <- copy left:number { done?:boolean <- greater-than column:number, right:number break-if done?:boolean print-character screen:address, 32/space column:number <- add column:number, 1 loop } ] scenario editor-initially-prints-multiple-lines [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc def] new-editor s:address:array:character, screen:address, 0/left, 5/right ] screen-should-contain [ . . .abc . .def . . . ] ] scenario editor-initially-handles-offsets [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc] new-editor s:address:array:character, screen:address, 1/left, 5/right ] screen-should-contain [ . . . abc . . . ] ] scenario editor-initially-prints-multiple-lines-at-offset [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc def] new-editor s:address:array:character, screen:address, 1/left, 5/right ] screen-should-contain [ . . . abc . . def . . . ] ] scenario editor-initially-wraps-long-lines [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc def] new-editor s:address:array:character, screen:address, 0/left, 5/right ] screen-should-contain [ . . .abc ↩. .def . . . ] screen-should-contain-in-color 245/grey [ . . . ↩. . . . . ] ] scenario editor-initially-wraps-barely-long-lines [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abcde] new-editor s:address:array:character, screen:address, 0/left, 5/right ] # still wrap, even though the line would fit. We need room to click on the # end of the line screen-should-contain [ . . .abcd↩. .e . . . ] screen-should-contain-in-color 245/grey [ . . . ↩. . . . . ] ] scenario editor-initializes-empty-text [ assume-screen 5/width, 5/height run [ 1:address:array:character <- new [] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . . . . . ] memory-should-contain [ 3 <- 1 # cursor row 4 <- 0 # cursor column ] ] ## highlighting mu code scenario render-colors-comments [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc # de f] new-editor s:address:array:character, screen:address, 0/left, 5/right ] screen-should-contain [ . . .abc . .# de . .f . . . ] screen-should-contain-in-color 12/lightblue, [ . . . . .# de . . . . . ] screen-should-contain-in-color 7/white, [ . . .abc . . . .f . . . ] ] # color:number, highlighting-state:number <- get-color color:number, highlighting-state:number, c:character recipe get-color [ local-scope color:number <- next-ingredient highlighting-state:number <- next-ingredient c:character <- next-ingredient color-is-white?:boolean <- equal color:number, 7/white #? $print [character: ], c:character, 10/newline #? 1 # if color is white and next character is '#', switch color to blue { break-unless color-is-white?:boolean starting-comment?:boolean <- equal c:character, 35/# break-unless starting-comment?:boolean #? $print [switch color back to blue], 10/newline #? 1 color:number <- copy 12/lightblue jump +exit:label } # if color is blue and next character is newline, switch color to white { color-is-blue?:boolean <- equal color:number, 12/lightblue break-unless color-is-blue?:boolean ending-comment?:boolean <- equal c:character, 10/newline break-unless ending-comment?:boolean #? $print [switch color back to white], 10/newline #? 1 color:number <- copy 7/white jump +exit:label } # if color is white (no comments) and next character is '<', switch color to red { break-unless color-is-white?:boolean starting-assignment?:boolean <- equal c:character, 60/< break-unless starting-assignment?:boolean color:number <- copy 1/red jump +exit:label } # if color is red and next character is space, switch color to white { color-is-red?:boolean <- equal color:number, 1/red break-unless color-is-red?:boolean ending-assignment?:boolean <- equal c:character, 32/space break-unless ending-assignment?:boolean color:number <- copy 7/white jump +exit:label } # otherwise no change +exit reply color:number, highlighting-state:number ] scenario render-colors-assignment [ assume-screen 8/width, 5/height run [ s:address:array:character <- new [abc d <- e f] new-editor s:address:array:character, screen:address, 0/left, 8/right ] screen-should-contain [ . . .abc . .d <- e . .f . . . ] screen-should-contain-in-color 1/red, [ . . . . . <- . . . . . ] ] ## handling events from the keyboard, mouse, touch screen, ... recipe event-loop [ local-scope screen:address <- next-ingredient console:address <- next-ingredient env:address:programming-environment-data <- next-ingredient recipes:address:editor-data <- get env:address:programming-environment-data/lookup, recipes:offset current-sandbox:address:editor-data <- get env:address:programming-environment-data/lookup, current-sandbox:offset sandbox-in-focus?:address:boolean <- get-address env:address:programming-environment-data/lookup, sandbox-in-focus?:offset { # looping over each (keyboard or touch) event as it occurs +next-event e:event, console:address, found?:boolean, quit?:boolean <- read-event console:address loop-unless found?:boolean break-if quit?:boolean # only in tests trace [app], [next-event] # check for global events that will trigger regardless of which editor has focus { k:address:number <- maybe-convert e:event, keycode:variant break-unless k:address:number # F4? load all code and run all sandboxes. { do-run?:boolean <- equal k:address:number/lookup, 65532/F4 break-unless do-run?:boolean run-sandboxes env:address:programming-environment-data # F4 might update warnings and results on both sides screen:address <- render-all screen:address, env:address:programming-environment-data update-cursor screen:address, recipes:address:editor-data, current-sandbox:address:editor-data, sandbox-in-focus?:address:boolean/lookup show-screen screen:address loop +next-event:label } } { c:address:character <- maybe-convert e:event, text:variant break-unless c:address:character # ctrl-n? - switch focus { ctrl-n?:boolean <- equal c:address:character/lookup, 14/ctrl-n break-unless ctrl-n?:boolean sandbox-in-focus?:address:boolean/lookup <- not sandbox-in-focus?:address:boolean/lookup update-cursor screen:address, recipes:address:editor-data, current-sandbox:address:editor-data, sandbox-in-focus?:address:boolean/lookup show-screen screen:address loop +next-event:label } } # 'touch' event { t:address:touch-event <- maybe-convert e:event, touch:variant break-unless t:address:touch-event # ignore all but 'left-click' events for now # todo: test this touch-type:number <- get t:address:touch-event/lookup, type:offset is-left-click?:boolean <- equal touch-type:number, 65513/mouse-left loop-unless is-left-click?:boolean, +next-event:label # on a sandbox delete icon? process delete { was-delete?:boolean <- delete-sandbox t:address:touch-event/lookup, env:address:programming-environment-data break-unless was-delete?:boolean #? trace [app], [delete clicked] #? 1 screen:address <- render-sandbox-side screen:address, env:address:programming-environment-data, 1/clear update-cursor screen:address, recipes:address:editor-data, current-sandbox:address:editor-data, sandbox-in-focus?:address:boolean/lookup show-screen screen:address loop +next-event:label } # if not, send to both editors _ <- move-cursor-in-editor screen:address, recipes:address:editor-data, t:address:touch-event/lookup sandbox-in-focus?:address:boolean/lookup <- move-cursor-in-editor screen:address, current-sandbox:address:editor-data, t:address:touch-event/lookup jump +continue:label } # if it's not global, send to appropriate editor { { break-if sandbox-in-focus?:address:boolean/lookup handle-event screen:address, console:address, recipes:address:editor-data, e:event } { break-unless sandbox-in-focus?:address:boolean/lookup handle-event screen:address, console:address, current-sandbox:address:editor-data, e:event } } +continue # if no more events currently left to process, render. # we rely on 'render' to update 'before-cursor' on pointer events, but # they won't usually come fast enough to trigger this. # todo: test this { more-events?:boolean <- has-more-events? console:address break-if more-events?:boolean render-minimal screen:address, env:address:programming-environment-data } loop } ] # helper for testing a single editor recipe editor-event-loop [ local-scope screen:address <- next-ingredient console:address <- next-ingredient editor:address:editor-data <- next-ingredient { # looping over each (keyboard or touch) event as it occurs +next-event e:event, console:address, found?:boolean, quit?:boolean <- read-event console:address loop-unless found?:boolean break-if quit?:boolean # only in tests trace [app], [next-event] # 'touch' event - send to both editors { t:address:touch-event <- maybe-convert e:event, touch:variant break-unless t:address:touch-event move-cursor-in-editor screen:address, editor:address:editor-data, t:address:touch-event/lookup jump +continue:label } # other events - send to appropriate editor handle-event screen:address, console:address, editor:address:editor-data, e:event +continue row:number, screen:address <- render screen:address, editor:address:editor-data # clear next line, in case we just processed a backspace left:number <- get editor:address:editor-data/lookup, left:offset right:number <- get editor:address:editor-data/lookup, right:offset row:number <- add row:number, 1 move-cursor screen:address, row:number, left:number clear-line-delimited screen:address, left:number, right:number loop } ] recipe handle-event [ local-scope screen:address <- next-ingredient console:address <- next-ingredient editor:address:editor-data <- next-ingredient e:event <- next-ingredient reply-unless editor:address:editor-data # character { c:address:character <- maybe-convert e:event, text:variant break-unless c:address:character ## check for special characters # backspace - delete character before cursor { backspace?:boolean <- equal c:address:character/lookup, 8/backspace break-unless backspace?:boolean delete-before-cursor editor:address:editor-data reply } # ctrl-a - move cursor to start of line { ctrl-a?:boolean <- equal c:address:character/lookup, 1/ctrl-a break-unless ctrl-a?:boolean move-to-start-of-line editor:address:editor-data reply } # ctrl-e - move cursor to end of line { ctrl-e?:boolean <- equal c:address:character/lookup, 5/ctrl-e break-unless ctrl-e?:boolean move-to-end-of-line editor:address:editor-data reply } # ctrl-u - delete until start of line (excluding cursor) { ctrl-u?:boolean <- equal c:address:character/lookup, 21/ctrl-u break-unless ctrl-u?:boolean delete-to-start-of-line editor:address:editor-data reply } # ctrl-k - delete until end of line (including cursor) { ctrl-k?:boolean <- equal c:address:character/lookup, 11/ctrl-k break-unless ctrl-k?:boolean delete-to-end-of-line editor:address:editor-data reply } # tab - insert two spaces { tab?:boolean <- equal c:address:character/lookup, 9/tab break-unless tab?:boolean insert-at-cursor editor:address:editor-data, 32/space, screen:address insert-at-cursor editor:address:editor-data, 32/space, screen:address reply } # otherwise type it in insert-at-cursor editor:address:editor-data, c:address:character/lookup, screen:address reply } # otherwise it's a special key k:address:number <- maybe-convert e:event, keycode:variant assert k:address:number, [event was of unknown type; neither keyboard nor mouse] d:address:duplex-list <- get editor:address:editor-data/lookup, data:offset before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/lookup, before-cursor:offset cursor-row:address:number <- get-address editor:address:editor-data/lookup, cursor-row:offset cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset screen-height:number <- screen-height screen:address left:number <- get editor:address:editor-data/lookup, left:offset right:number <- get editor:address:editor-data/lookup, right:offset # arrows; update cursor-row and cursor-column, leave before-cursor to 'render'. # right arrow { move-to-next-character?:boolean <- equal k:address:number/lookup, 65514/right-arrow break-unless move-to-next-character?:boolean # if not at end of text old-cursor:address:duplex-list <- next-duplex before-cursor:address:address:duplex-list/lookup break-unless old-cursor:address:duplex-list # scan to next character before-cursor:address:address:duplex-list/lookup <- copy old-cursor:address:duplex-list # if crossed a newline, move cursor to start of next row { old-cursor-character:character <- get before-cursor:address:address:duplex-list/lookup/lookup, value:offset was-at-newline?:boolean <- equal old-cursor-character:character, 10/newline break-unless was-at-newline?:boolean cursor-row:address:number/lookup <- add cursor-row:address:number/lookup, 1 cursor-column:address:number/lookup <- copy left:number # todo: what happens when cursor is too far down? screen-height:number <- screen-height screen:address above-screen-bottom?:boolean <- lesser-than cursor-row:address:number/lookup, screen-height:number assert above-screen-bottom?:boolean, [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:number, 1 at-wrap?:boolean <- equal cursor-column:address:number/lookup, wrap-column:number break-unless at-wrap?:boolean # and if next character isn't newline new-cursor:address:duplex-list <- next-duplex old-cursor:address:duplex-list break-unless new-cursor:address:duplex-list next-character:character <- get new-cursor:address:duplex-list/lookup, value:offset newline?:boolean <- equal next-character:character, 10/newline break-if newline?:boolean cursor-row:address:number/lookup <- add cursor-row:address:number/lookup, 1 cursor-column:address:number/lookup <- copy left:number # todo: what happens when cursor is too far down? above-screen-bottom?:boolean <- lesser-than cursor-row:address:number/lookup, screen-height:number assert above-screen-bottom?:boolean, [unimplemented: moving past bottom of screen] reply } # otherwise move cursor one character right cursor-column:address:number/lookup <- add cursor-column:address:number/lookup, 1 } # left arrow { move-to-previous-character?:boolean <- equal k:address:number/lookup, 65515/left-arrow break-unless move-to-previous-character?:boolean #? trace [app], [left arrow] #? 1 # if not at start of text (before-cursor at § sentinel) prev:address:duplex-list <- prev-duplex before-cursor:address:address:duplex-list/lookup break-unless prev:address:duplex-list # if cursor not at left margin, move one character left { at-left-margin?:boolean <- equal cursor-column:address:number/lookup, 0 break-if at-left-margin?:boolean #? trace [app], [decrementing] #? 1 cursor-column:address:number/lookup <- subtract cursor-column:address:number/lookup, 1 reply } # if at left margin, there's guaranteed to be a previous line, since we're # not at start of text { # if before-cursor is at newline, figure out how long the previous line is prevc:character <- get before-cursor:address:address:duplex-list/lookup/lookup, value:offset previous-character-is-newline?:boolean <- equal prevc:character, 10/newline break-unless previous-character-is-newline?:boolean #? trace [app], [previous line] #? 1 # compute length of previous line end-of-line:number <- previous-line-length before-cursor:address:address:duplex-list/lookup, d:address:duplex-list cursor-row:address:number/lookup <- subtract cursor-row:address:number/lookup, 1 cursor-column:address:number/lookup <- copy end-of-line:number reply } # if before-cursor is not at newline, we're just at a wrapped line assert cursor-row:address:number/lookup, [unimplemented: moving cursor above top of screen] cursor-row:address:number/lookup <- subtract cursor-row:address:number/lookup, 1 cursor-column:address:number/lookup <- subtract right:number, 1 # leave room for wrap icon } # down arrow { move-to-next-line?:boolean <- equal k:address:number/lookup, 65516/down-arrow break-unless move-to-next-line?:boolean # todo: support scrolling already-at-bottom?:boolean <- greater-or-equal cursor-row:address:number/lookup, screen-height:number break-if already-at-bottom?:boolean #? $print [moving down #? ] #? 1 cursor-row:address:number/lookup <- add cursor-row:address:number/lookup, 1 # that's it; render will adjust cursor-column as necessary } # up arrow { move-to-previous-line?:boolean <- equal k:address:number/lookup, 65517/up-arrow break-unless move-to-previous-line?:boolean # todo: support scrolling already-at-top?:boolean <- lesser-or-equal cursor-row:address:number/lookup, 1/top break-if already-at-top?:boolean #? $print [moving up #? ] #? 1 cursor-row:address:number/lookup <- subtract cursor-row:address:number/lookup, 1 # that's it; render will adjust cursor-column as necessary } # home { home?:boolean <- equal k:address:number/lookup, 65521/home break-unless home?:boolean move-to-start-of-line editor:address:editor-data reply } # end { end?:boolean <- equal k:address:number/lookup, 65520/end break-unless end?:boolean move-to-end-of-line editor:address:editor-data reply } ] # process click, return if it was on current editor # todo: ignores menu bar (for now just displays shortcuts) recipe move-cursor-in-editor [ local-scope screen:address <- next-ingredient editor:address:editor-data <- next-ingredient t:touch-event <- next-ingredient reply-unless editor:address:editor-data, 0/false click-column:number <- get t:touch-event, column:offset left:number <- get editor:address:editor-data/lookup, left:offset too-far-left?:boolean <- lesser-than click-column:number, left:number reply-if too-far-left?:boolean, 0/false right:number <- get editor:address:editor-data/lookup, right:offset too-far-right?:boolean <- greater-than click-column:number, right:number reply-if too-far-right?:boolean, 0/false # update cursor cursor-row:address:number <- get-address editor:address:editor-data/lookup, cursor-row:offset cursor-row:address:number/lookup <- get t:touch-event, row:offset cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset cursor-column:address:number/lookup <- get t:touch-event, column:offset # gain focus reply 1/true ] recipe insert-at-cursor [ local-scope editor:address:editor-data <- next-ingredient c:character <- next-ingredient screen:address <- next-ingredient #? $print [insert ], c:character, 10/newline #? 1 before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/lookup, before-cursor:offset insert-duplex c:character, before-cursor:address:address:duplex-list/lookup before-cursor:address:address:duplex-list/lookup <- next-duplex before-cursor:address:address:duplex-list/lookup cursor-row:address:number <- get-address editor:address:editor-data/lookup, cursor-row:offset cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset left:number <- get editor:address:editor-data/lookup, left:offset right:number <- get editor:address:editor-data/lookup, right:offset # update cursor: if newline, move cursor to start of next line # todo: bottom of screen { newline?:boolean <- equal c:character, 10/newline break-unless newline?:boolean cursor-row:address:number/lookup <- add cursor-row:address:number/lookup, 1 cursor-column:address:number/lookup <- copy left:number # indent if necessary #? $print [computing indent], 10/newline #? 1 d:address:duplex-list <- get editor:address:editor-data/lookup, data:offset end-of-previous-line:address:duplex-list <- prev-duplex before-cursor:address:address:duplex-list/lookup indent:number <- line-indent end-of-previous-line:address:duplex-list, d:address:duplex-list #? $print indent:number, 10/newline #? 1 i:number <- copy 0 { indent-done?:boolean <- greater-or-equal i:number, indent:number break-if indent-done?:boolean insert-at-cursor editor:address:editor-data, 32/space, screen:address i:number <- add i:number, 1 loop } reply } # if the line wraps at the cursor, move cursor to start of next row { # if we're at the column just before the wrap indicator wrap-column:number <- subtract right:number, 1 #? $print [wrap? ], cursor-column:address:number/lookup, [ vs ], wrap-column:number, 10/newline at-wrap?:boolean <- greater-or-equal cursor-column:address:number/lookup, wrap-column:number break-unless at-wrap?:boolean #? $print [wrap! #? ] #? 1 cursor-column:address:number/lookup <- subtract cursor-column:address:number/lookup, wrap-column:number cursor-row:address:number/lookup <- add cursor-row:address:number/lookup, 1 # todo: what happens when cursor is too far down? screen-height:number <- screen-height screen:address above-screen-bottom?:boolean <- lesser-than cursor-row:address:number/lookup, screen-height:number assert above-screen-bottom?:boolean, [unimplemented: typing past bottom of screen] #? $print [return #? ] #? 1 reply } # otherwise move cursor right cursor-column:address:number/lookup <- add cursor-column:address:number/lookup, 1 ] recipe delete-before-cursor [ local-scope editor:address:editor-data <- next-ingredient before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/lookup, before-cursor:offset d:address:duplex-list <- get editor:address:editor-data/lookup, data:offset # unless already at start at-start?:boolean <- equal before-cursor:address:address:duplex-list/lookup, d:address:duplex-list reply-if at-start?:boolean # delete character prev:address:duplex-list <- prev-duplex before-cursor:address:address:duplex-list/lookup remove-duplex before-cursor:address:address:duplex-list/lookup # update cursor before-cursor:address:address:duplex-list/lookup <- copy prev:address:duplex-list cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset cursor-column:address:number/lookup <- subtract cursor-column:address:number/lookup, 1 #? $print [delete-before-cursor: ], cursor-column:address:number/lookup, 10/newline ] # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts # the length of the previous line before the 'curr' pointer. recipe previous-line-length [ local-scope curr:address:duplex-list <- next-ingredient start:address:duplex-list <- next-ingredient result:number <- copy 0 reply-unless curr:address:duplex-list, result:number at-start?:boolean <- equal curr:address:duplex-list, start:address:duplex-list reply-if at-start?:boolean, result:number { curr:address:duplex-list <- prev-duplex curr:address:duplex-list break-unless curr:address:duplex-list at-start?:boolean <- equal curr:address:duplex-list, start:address:duplex-list break-if at-start?:boolean c:character <- get curr:address:duplex-list/lookup, value:offset at-newline?:boolean <- equal c:character 10/newline break-if at-newline?:boolean result:number <- add result:number, 1 loop } reply result:number ] # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts # the number of spaces at the start of the line containing 'curr'. recipe line-indent [ local-scope curr:address:duplex-list <- next-ingredient start:address:duplex-list <- next-ingredient result:number <- copy 0 reply-unless curr:address:duplex-list, result:number #? $print [a0], 10/newline #? 1 at-start?:boolean <- equal curr:address:duplex-list, start:address:duplex-list reply-if at-start?:boolean, result:number #? $print [a1], 10/newline #? 1 { curr:address:duplex-list <- prev-duplex curr:address:duplex-list break-unless curr:address:duplex-list #? $print [a2], 10/newline #? 1 at-start?:boolean <- equal curr:address:duplex-list, start:address:duplex-list break-if at-start?:boolean #? $print [a3], 10/newline #? 1 c:character <- get curr:address:duplex-list/lookup, value:offset at-newline?:boolean <- equal c:character, 10/newline break-if at-newline?:boolean #? $print [a4], 10/newline #? 1 # if c is a space, increment result is-space?:boolean <- equal c:character, 32/space { break-unless is-space?:boolean result:number <- add result:number, 1 } # if c is not a space, reset result { break-if is-space?:boolean result:number <- copy 0 } loop } reply result:number ] recipe move-to-start-of-line [ local-scope editor:address:editor-data <- next-ingredient # update cursor column left:number <- get editor:address:editor-data/lookup, left:offset cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset cursor-column:address:number/lookup <- copy left:number # update before-cursor before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/lookup, before-cursor:offset init:address:duplex-list <- get editor:address:editor-data/lookup, data:offset # while not at start of line, move { at-start-of-text?:boolean <- equal before-cursor:address:address:duplex-list/lookup, init:address:duplex-list break-if at-start-of-text?:boolean prev:character <- get before-cursor:address:address:duplex-list/lookup/lookup, value:offset at-start-of-line?:boolean <- equal prev:character, 10/newline break-if at-start-of-line?:boolean before-cursor:address:address:duplex-list/lookup <- prev-duplex before-cursor:address:address:duplex-list/lookup assert before-cursor:address:address:duplex-list/lookup, [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:address:editor-data/lookup, before-cursor:offset cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset # while not at start of line, move { next:address:duplex-list <- next-duplex before-cursor:address:address:duplex-list/lookup break-unless next:address:duplex-list # end of text nextc:character <- get next:address:duplex-list/lookup, value:offset at-end-of-line?:boolean <- equal nextc:character, 10/newline break-if at-end-of-line?:boolean before-cursor:address:address:duplex-list/lookup <- copy next:address:duplex-list cursor-column:address:number/lookup <- add cursor-column:address:number/lookup, 1 loop } # move one past end of line cursor-column:address:number/lookup <- add cursor-column:address:number/lookup, 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:address:editor-data/lookup, data:offset before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/lookup, before-cursor:offset start:address:duplex-list <- copy before-cursor:address:address:duplex-list/lookup end:address:duplex-list <- next-duplex before-cursor:address:address:duplex-list/lookup { at-start-of-text?:boolean <- equal start:address:duplex-list, init:address:duplex-list break-if at-start-of-text?:boolean curr:character <- get start:address:duplex-list/lookup, value:offset at-start-of-line?:boolean <- equal curr:character, 10/newline break-if at-start-of-line?:boolean start:address:duplex-list <- prev-duplex start:address:duplex-list assert start:address:duplex-list, [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:address:duplex-list/lookup, next:offset start-next:address:address:duplex-list/lookup <- copy end:address:duplex-list end-prev:address:address:duplex-list <- get-address end:address:duplex-list/lookup, prev:offset end-prev:address:address:duplex-list/lookup <- copy start:address:duplex-list # adjust cursor before-cursor:address:address:duplex-list/lookup <- prev-duplex end:address:duplex-list left:number <- get editor:address:editor-data/lookup, left:offset cursor-column:address:number <- get-address editor:address:editor-data/lookup, cursor-column:offset cursor-column:address:number/lookup <- copy left:number ] recipe delete-to-end-of-line [ local-scope editor:address:editor-data <- next-ingredient # compute range to delete start:address:duplex-list <- get editor:address:editor-data/lookup, before-cursor:offset end:address:duplex-list <- next-duplex start:address:duplex-list { at-end-of-text?:boolean <- equal end:address:duplex-list, 0/null break-if at-end-of-text?:boolean curr:character <- get end:address:duplex-list/lookup, value:offset at-end-of-line?:boolean <- equal curr:character, 10/newline break-if at-end-of-line?:boolean end:address:duplex-list <- next-duplex end:address:duplex-list loop } # snip it out start-next:address:address:duplex-list <- get-address start:address:duplex-list/lookup, next:offset start-next:address:address:duplex-list/lookup <- copy end:address:duplex-list { break-unless end:address:duplex-list end-prev:address:address:duplex-list <- get-address end:address:duplex-list/lookup, prev:offset end-prev:address:address:duplex-list/lookup <- copy start:address:duplex-list } ] recipe render-all [ local-scope screen:address <- next-ingredient env:address:programming-environment-data <- next-ingredient screen:address <- render-recipes screen:address, env:address:programming-environment-data, 1/clear-below screen:address <- render-sandbox-side screen:address, env:address:programming-environment-data, 1/clear-below recipes:address:editor-data <- get env:address:programming-environment-data/lookup, recipes:offset current-sandbox:address:editor-data <- get env:address:programming-environment-data/lookup, current-sandbox:offset sandbox-in-focus?:boolean <- get env:address:programming-environment-data/lookup, sandbox-in-focus?:offset update-cursor screen:address, recipes:address:editor-data, current-sandbox:address:editor-data, sandbox-in-focus?:boolean show-screen screen:address reply screen:address/same-as-ingredient:0 ] recipe render-minimal [ local-scope screen:address <- next-ingredient env:address:programming-environment-data <- next-ingredient recipes:address:editor-data <- get env:address:programming-environment-data/lookup, recipes:offset current-sandbox:address:editor-data <- get env:address:programming-environment-data/lookup, current-sandbox:offset sandbox-in-focus?:boolean <- get env:address:programming-environment-data/lookup, sandbox-in-focus?:offset { break-if sandbox-in-focus?:boolean screen:address <- render-recipes screen:address, env:address:programming-environment-data cursor-row:number <- get recipes:address:editor-data/lookup, cursor-row:offset cursor-column:number <- get recipes:address:editor-data/lookup, cursor-column:offset } { break-unless sandbox-in-focus?:boolean screen:address <- render-sandbox-side screen:address, env:address:programming-environment-data cursor-row:number <- get current-sandbox:address:editor-data/lookup, cursor-row:offset cursor-column:number <- get current-sandbox:address:editor-data/lookup, cursor-column:offset } move-cursor screen:address, cursor-row:number, cursor-column:number show-screen screen:address reply screen:address/same-as-ingredient:0 ] recipe render-recipes [ local-scope screen:address <- next-ingredient env:address:programming-environment-data <- next-ingredient clear:boolean <- next-ingredient recipes:address:editor-data <- get env:address:programming-environment-data/lookup, recipes:offset # render recipes left:number <- get recipes:address:editor-data/lookup, left:offset right:number <- get recipes:address:editor-data/lookup, right:offset row:number, screen:address <- render screen:address, recipes:address:editor-data recipe-warnings:address:array:character <- get env:address:programming-environment-data/lookup, recipe-warnings:offset { # print any warnings break-unless recipe-warnings:address:array:character row:number, screen:address <- render-string screen:address, recipe-warnings:address:array:character, left:number, right:number, 1/red, row:number } { # no warnings? move to next line break-if recipe-warnings:address:array:character row:number <- add row:number, 1 } # draw dotted line after recipes draw-horizontal screen:address, row:number, left:number, right:number, 9480/horizontal-dotted # clear next line, in case we just processed a backspace row:number <- add row:number, 1 move-cursor screen:address, row:number, left:number clear-line-delimited screen:address, left:number, right:number # clear rest of screen in this column, if requested reply-unless clear:boolean, screen:address/same-as-ingredient:0 screen-height:number <- screen-height screen:address { at-bottom-of-screen?:boolean <- greater-or-equal row:number, screen-height:number break-if at-bottom-of-screen?:boolean move-cursor screen:address, row:number, left:number clear-line-delimited screen:address, left:number, right:number row:number <- add row:number, 1 loop } reply screen:address/same-as-ingredient:0 ] recipe update-cursor [ local-scope screen:address <- next-ingredient recipes:address:editor-data <- next-ingredient current-sandbox:address:editor-data <- next-ingredient sandbox-in-focus?:boolean <- next-ingredient { break-if sandbox-in-focus?:boolean #? $print [recipes in focus #? ] #? 1 cursor-row:number <- get recipes:address:editor-data/lookup, cursor-row:offset cursor-column:number <- get recipes:address:editor-data/lookup, cursor-column:offset } { break-unless sandbox-in-focus?:boolean #? $print [sandboxes in focus #? ] #? 1 cursor-row:number <- get current-sandbox:address:editor-data/lookup, cursor-row:offset cursor-column:number <- get current-sandbox:address:editor-data/lookup, cursor-column:offset } move-cursor screen:address, cursor-row:number, cursor-column:number ] scenario editor-handles-empty-event-queue [ 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 [] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . . . ] ] scenario editor-handles-mouse-clicks [ 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 [ left-click 1, 1 # on the 'b' ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . .abc . . . ] memory-should-contain [ 3 <- 1 # cursor is at row 0.. 4 <- 1 # ..and column 1 ] ] scenario editor-handles-mouse-clicks-outside-text [ 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 [ left-click 1, 7 # last line, to the right of text ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 1 # cursor row 4 <- 3 # cursor column ] ] scenario editor-handles-mouse-clicks-outside-text-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc def] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right assume-console [ left-click 1, 7 # interior line, to the right of text ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 1 # cursor row 4 <- 3 # cursor column ] ] scenario editor-handles-mouse-clicks-outside-text-3 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc def] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right assume-console [ left-click 3, 7 # below text ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 2 # cursor row 4 <- 3 # cursor column ] ] scenario editor-handles-mouse-clicks-outside-column [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc] # editor occupies only left half of screen 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right assume-console [ # click on right half of screen left-click 3, 8 ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . .abc . . . ] memory-should-contain [ 3 <- 1 # no change to cursor row 4 <- 0 # ..or column ] ] scenario editor-inserts-characters-into-empty-editor [ assume-screen 10/width, 5/height 1:address:array:character <- new [] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right assume-console [ type [abc] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . . . ] ] scenario editor-inserts-characters-at-cursor [ 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 [ type [0] left-click 1, 2 type [d] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .0adbc . . . ] ] scenario editor-inserts-characters-at-cursor-2 [ 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 [ left-click 1, 5 # right of last line type [d] # should append ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abcd . . . ] ] scenario editor-inserts-characters-at-cursor-3 [ 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 [ left-click 3, 5 # below all text type [d] # should append ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abcd . . . ] ] scenario editor-inserts-characters-at-cursor-4 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc d] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right assume-console [ left-click 3, 5 # below all text type [e] # should append ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . .de . . . ] ] scenario editor-inserts-characters-at-cursor-5 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc d] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right assume-console [ left-click 3, 5 # below all text type [ef] # should append multiple characters in order ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . .def . . . ] ] scenario editor-wraps-line-on-insert [ assume-screen 5/width, 5/height 1:address:array:character <- new [abc] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right # type a letter assume-console [ type [e] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # no wrap yet screen-should-contain [ . . .eabc . . . . . ] # type a second letter assume-console [ type [f] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # now wrap screen-should-contain [ . . .efab↩. .c . . . ] ] scenario editor-moves-cursor-after-inserting-characters [ assume-screen 10/width, 5/height 1:address:array:character <- new [ab] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right assume-console [ type [01] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .01ab . . . ] ] scenario editor-wraps-cursor-after-inserting-characters [ assume-screen 10/width, 5/height 1:address:array:character <- new [abcde] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right assume-console [ left-click 1, 4 # line is full; no wrap icon yet type [f] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . .abcd↩ . .fe . . . ] memory-should-contain [ 3 <- 2 # cursor row 4 <- 1 # cursor column ] ] scenario editor-wraps-cursor-after-inserting-characters-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abcde] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right assume-console [ left-click 1, 3 # right before the wrap icon type [f] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . .abcf↩ . .de . . . ] memory-should-contain [ 3 <- 2 # cursor row 4 <- 0 # cursor column ] ] scenario editor-moves-cursor-down-after-inserting-newline [ 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 [ type [0 1] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .0 . .1abc . . . ] ] scenario editor-moves-cursor-down-after-inserting-newline-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 1/left, 10/right assume-console [ type [0 1] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . . 0 . . 1abc . . . ] ] scenario editor-clears-previous-line-completely-after-inserting-newline [ assume-screen 10/width, 5/height 1:address:array:character <- new [abcde] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right # press just a 'newline' assume-console [ type [ ] ] screen-should-contain [ . . .abcd↩ . .e . . . . . ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # line should be fully cleared screen-should-contain [ . . . . .abcd↩ . .e . . . ] ] scenario editor-inserts-indent-after-newline [ assume-screen 10/width, 10/height 1:address:array:character <- new [ab cd ef] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # position cursor after 'cd' and hit 'newline' assume-console [ left-click 2, 8 type [ ] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor should be below start of previous line memory-should-contain [ 3 <- 3 # cursor row 4 <- 2 # cursor column (indented) ] ] scenario editor-handles-backspace-key [ 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 [ left-click 1, 1 type [«] ] 3:event/backspace <- merge 0/text, 8/backspace, 0/dummy, 0/dummy replace-in-console 171/«, 3:event/backspace run [ editor-event-loop screen:address, console:address, 2:address:editor-data 4:number <- get 2:address:editor-data/lookup, cursor-row:offset 5:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . .bc . . . ] memory-should-contain [ 4 <- 1 5 <- 0 ] ] scenario editor-clears-last-line-on-backspace [ 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 assume-console [ left-click 2, 0 # cursor at only character in final line type [«] ] 3:event/backspace <- merge 0/text, 8/backspace, 0/dummy, 0/dummy replace-in-console 171/«, 3:event/backspace run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abcd . . . ] ] scenario editor-inserts-two-spaces-on-tab [ 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 assume-console [ 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 [ . . . ab . .cd . ] ] scenario editor-moves-cursor-right-with-key [ 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 65514 # right arrow type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .a0bc . . . ] ] scenario editor-moves-cursor-to-next-line-with-right-arrow [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc d] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right assume-console [ press 65514 # right arrow press 65514 # right arrow press 65514 # right arrow press 65514 # right arrow - next line type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . .0d . . . ] ] scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc d] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 1/left, 10/right assume-console [ press 65514 # right arrow press 65514 # right arrow press 65514 # right arrow press 65514 # right arrow - next line type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . . abc . . 0d . . . ] ] scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [ assume-screen 10/width, 5/height 1:address:array:character <- new [abcdef] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right assume-console [ left-click 1, 3 press 65514 # right arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . .abcd↩ . .ef . . . ] memory-should-contain [ 3 <- 2 4 <- 0 ] ] scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [ assume-screen 10/width, 5/height # line just barely wrapping 1:address:array:character <- new [abcde] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right # position cursor at last character before wrap and hit right-arrow assume-console [ left-click 1, 3 press 65514 # right arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 2 4 <- 0 ] # now hit right arrow again assume-console [ press 65514 ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 2 4 <- 1 ] ] scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abcdef] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 1/left, 6/right assume-console [ left-click 1, 4 press 65514 # right arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . . . abcd↩ . . ef . . . ] memory-should-contain [ 3 <- 2 4 <- 1 ] ] scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc d] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right assume-console [ left-click 1, 3 press 65514 # right arrow - next line type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . .0d . . . ] ] scenario editor-moves-cursor-left-with-key [ 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 [ left-click 1, 2 press 65515 # left arrow type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .a0bc . . . ] ] 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 1:address:array:character <- new [abc d] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # position cursor at start of second line (so there's no previous newline) assume-console [ left-click 2, 0 press 65515 # left arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 1 4 <- 3 ] ] scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [ assume-screen 10/width, 5/height # initialize editor with three lines 1:address:array:character <- new [abc def g] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # position cursor further down (so there's a newline before the character at # the cursor) assume-console [ left-click 3, 0 press 65515 # left arrow type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . .def0 . .g . . . ] ] scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc def g] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # position cursor at start of text assume-console [ left-click 1, 0 press 65515 # left arrow should have no effect type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .0abc . .def . .g . . . ] ] scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [ assume-screen 10/width, 5/height # initialize editor with text containing an empty line 1:address:array:character <- new [abc d] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # position cursor right after empty line assume-console [ left-click 3, 0 press 65515 # left arrow type [0] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] screen-should-contain [ . . .abc . .0 . .d . . . ] ] scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [ assume-screen 10/width, 5/height # initialize editor with text containing an empty line 1:address:array:character <- new [abcdef] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right screen-should-contain [ . . .abcd↩ . .ef . . . ] # position cursor right after empty line assume-console [ left-click 2, 0 press 65515 # left arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 1 # previous row 4 <- 3 # end of wrapped line ] ] scenario editor-moves-to-previous-line-with-up-arrow [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc def] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right assume-console [ left-click 2, 1 press 65517 # up arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 1 4 <- 1 ] ] scenario editor-moves-to-next-line-with-down-arrow [ assume-screen 10/width, 5/height 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 [ press 65516 # down arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # ..and ends at (2, 0) memory-should-contain [ 3 <- 2 4 <- 0 ] ] scenario editor-adjusts-column-at-previous-line [ assume-screen 10/width, 5/height 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 2, 3 press 65517 # up arrow ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 1 4 <- 2 ] ] scenario editor-adjusts-column-at-next-line [ assume-screen 10/width, 5/height 1:address:array:character <- new [abc de] 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 ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 3 <- 2 4 <- 2 ] ] scenario editor-moves-to-start-of-line-with-ctrl-a [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on second line, press ctrl-a assume-console [ left-click 2, 3 type [a] # ctrl-a ] 3:event/ctrl-a <- merge 0/text, 1/ctrl-a, 0/dummy, 0/dummy replace-in-console 97/a, 3:event/ctrl-a run [ editor-event-loop screen:address, console:address, 2:address:editor-data 4:number <- get 2:address:editor-data/lookup, cursor-row:offset 5:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to start of line memory-should-contain [ 4 <- 2 5 <- 0 ] ] scenario editor-moves-to-start-of-line-with-ctrl-a-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on first line (no newline before), press ctrl-a assume-console [ left-click 1, 3 type [a] # ctrl-a ] 3:event/ctrl-a <- merge 0/text, 1/ctrl-a, 0/dummy, 0/dummy replace-in-console 97/a, 3:event/ctrl-a run [ editor-event-loop screen:address, console:address, 2:address:editor-data 4:number <- get 2:address:editor-data/lookup, cursor-row:offset 5:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to start of line memory-should-contain [ 4 <- 1 5 <- 0 ] ] scenario editor-moves-to-start-of-line-with-home [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on second line, press 'home' assume-console [ left-click 2, 3 press 65521 # 'home' ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to start of line memory-should-contain [ 3 <- 2 4 <- 0 ] ] scenario editor-moves-to-start-of-line-with-home-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on first line (no newline before), press 'home' assume-console [ left-click 1, 3 press 65521 # 'home' ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to start of line memory-should-contain [ 3 <- 1 4 <- 0 ] ] scenario editor-moves-to-start-of-line-with-ctrl-e [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on first line, press ctrl-e assume-console [ left-click 1, 1 type [e] # ctrl-e ] 3:event/ctrl-e <- merge 0/text, 5/ctrl-e, 0/dummy, 0/dummy replace-in-console 101/e, 3:event/ctrl-e run [ editor-event-loop screen:address, console:address, 2:address:editor-data 4:number <- get 2:address:editor-data/lookup, cursor-row:offset 5:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to end of line memory-should-contain [ 4 <- 1 5 <- 3 ] # editor inserts future characters at cursor assume-console [ type [z] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 4:number <- get 2:address:editor-data/lookup, cursor-row:offset 5:number <- get 2:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 4 <- 1 5 <- 4 ] screen-should-contain [ . . .123z . .456 . . . ] ] scenario editor-moves-to-end-of-line-with-ctrl-e-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on second line (no newline after), press ctrl-e assume-console [ left-click 2, 1 type [e] # ctrl-e ] 3:event/ctrl-e <- merge 0/text, 5/ctrl-e, 0/dummy, 0/dummy replace-in-console 101/e, 3:event/ctrl-e run [ editor-event-loop screen:address, console:address, 2:address:editor-data 4:number <- get 2:address:editor-data/lookup, cursor-row:offset 5:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to end of line memory-should-contain [ 4 <- 2 5 <- 3 ] ] scenario editor-moves-to-end-of-line-with-end [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on first line, press 'end' assume-console [ left-click 1, 1 press 65520 # 'end' ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to end of line memory-should-contain [ 3 <- 1 4 <- 3 ] ] scenario editor-moves-to-end-of-line-with-end-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on second line (no newline after), press 'end' assume-console [ left-click 2, 1 press 65520 # 'end' ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:number <- get 2:address:editor-data/lookup, cursor-row:offset 4:number <- get 2:address:editor-data/lookup, cursor-column:offset ] # cursor moves to end of line memory-should-contain [ 3 <- 2 4 <- 3 ] ] scenario editor-deletes-to-start-of-line-with-ctrl-u [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on second line, press ctrl-u assume-console [ left-click 2, 2 type [u] # ctrl-u ] 3:event/ctrl-a <- 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 ] # cursor deletes to start of line screen-should-contain [ . . .123 . .6 . . . ] ] scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on first line (no newline before), press ctrl-u assume-console [ 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 run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to start of line screen-should-contain [ . . .3 . .456 . . . ] ] scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start past end of line, press ctrl-u assume-console [ 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 run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to start of line screen-should-contain [ . . . . .456 . . . ] ] scenario editor-deletes-to-end-of-line-with-ctrl-k [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on first line, press ctrl-k assume-console [ left-click 1, 1 type [k] # ctrl-k ] 3:event/ctrl-k <- merge 0/text, 11/ctrl-k, 0/dummy, 0/dummy replace-in-console 107/k, 3:event/ctrl-k run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to end of line screen-should-contain [ . . .1 . .456 . . . ] ] scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start on second line (no newline after), press ctrl-k assume-console [ left-click 2, 1 type [k] # ctrl-k ] 3:event/ctrl-k <- merge 0/text, 11/ctrl-k, 0/dummy, 0/dummy replace-in-console 107/k, 3:event/ctrl-k run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to end of line screen-should-contain [ . . .123 . .4 . . . ] ] scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start at end of line assume-console [ left-click 1, 2 type [k] # ctrl-k ] 3:event/ctrl-k <- merge 0/text, 11/ctrl-k, 0/dummy, 0/dummy replace-in-console 107/k, 3:event/ctrl-k run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to end of line screen-should-contain [ . . .12 . .456 . . . ] ] scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start past end of line assume-console [ left-click 1, 3 type [k] # ctrl-k ] 3:event/ctrl-k <- merge 0/text, 11/ctrl-k, 0/dummy, 0/dummy replace-in-console 107/k, 3:event/ctrl-k run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to end of line screen-should-contain [ . . .123 . .456 . . . ] ] scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start at end of text assume-console [ left-click 2, 2 type [k] # ctrl-k ] 3:event/ctrl-k <- merge 0/text, 11/ctrl-k, 0/dummy, 0/dummy replace-in-console 107/k, 3:event/ctrl-k run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to end of line screen-should-contain [ . . .123 . .45 . . . ] ] scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [ assume-screen 10/width, 5/height 1:address:array:character <- new [123 456] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right # start past end of text assume-console [ left-click 2, 3 type [k] # ctrl-k ] 3:event/ctrl-k <- merge 0/text, 11/ctrl-k, 0/dummy, 0/dummy replace-in-console 107/k, 3:event/ctrl-k run [ editor-event-loop screen:address, console:address, 2:address:editor-data ] # cursor deletes to end of line screen-should-contain [ . . .123 . .456 . . . ] ] scenario point-at-multiple-editors [ $close-trace assume-screen 30/width, 5/height # initialize both halves of screen 1:address:array:character <- new [abc] 2:address:array:character <- new [def] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # focus on both sides assume-console [ left-click 1, 1 left-click 1, 17 ] # check cursor column in each run [ event-loop screen:address, console:address, 3:address:programming-environment-data 4:address:editor-data <- get 3:address:programming-environment-data/lookup, recipes:offset 5:number <- get 4:address:editor-data/lookup, cursor-column:offset 6:address:editor-data <- get 3:address:programming-environment-data/lookup, current-sandbox:offset 7:number <- get 6:address:editor-data/lookup, cursor-column:offset ] memory-should-contain [ 5 <- 1 7 <- 17 ] ] scenario edit-multiple-editors [ $close-trace assume-screen 30/width, 5/height # initialize both halves of screen 1:address:array:character <- new [abc] 2:address:array:character <- new [def] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # type one letter in each of them assume-console [ left-click 1, 1 type [0] left-click 1, 17 type [1] ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data 4:address:editor-data <- get 3:address:programming-environment-data/lookup, recipes:offset 5:number <- get 4:address:editor-data/lookup, cursor-column:offset 6:address:editor-data <- get 3:address:programming-environment-data/lookup, current-sandbox:offset 7:number <- get 6:address:editor-data/lookup, cursor-column:offset ] screen-should-contain [ . run (F4) . # this line has a different background, but we don't test that yet .a0bc ┊d1ef . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━. . ┊ . ] memory-should-contain [ 5 <- 2 # cursor column of recipe editor 7 <- 18 # cursor column of sandbox editor ] # show the cursor at the right window run [ screen:address <- print-character screen:address, 9251/␣ ] screen-should-contain [ . run (F4) . .a0bc ┊d1␣f . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━. . ┊ . ] ] scenario multiple-editors-cover-only-their-own-areas [ $close-trace assume-screen 60/width, 10/height run [ 1:address:array:character <- new [abc] 2:address:array:character <- new [def] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character ] # divider isn't messed up screen-should-contain [ . run (F4) . .abc ┊def . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . . ┊ . ] ] scenario editor-in-focus-keeps-cursor [ $close-trace assume-screen 30/width, 5/height 1:address:array:character <- new [abc] 2:address:array:character <- new [def] # initialize programming environment and highlight cursor assume-console [] run [ 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character event-loop screen:address, console:address, 3:address:programming-environment-data screen:address <- print-character screen:address, 9251/␣ ] # is cursor at the right place? screen-should-contain [ . run (F4) . .␣bc ┊def . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━. . ┊ . ] # now try typing a letter assume-console [ type [z] ] run [ 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character event-loop screen:address, console:address, 3:address:programming-environment-data screen:address <- print-character screen:address, 9251/␣ ] # cursor should still be right screen-should-contain [ . run (F4) . .z␣bc ┊def . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━. . ┊ . ] ] ## Running code from the editors container sandbox-data [ data:address:array:character response:address:array:character warnings:address:array:character starting-row-on-screen:number # to track clicks on delete screen:address:screen # prints in the sandbox go here next-sandbox:address:sandbox-data ] scenario run-and-show-results [ $close-trace # trace too long for github assume-screen 100/width, 15/height # recipe editor is empty 1:address:array:character <- new [] # sandbox editor contains an instruction without storing outputs 2:address:array:character <- new [divide-with-remainder 11, 3] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # run the code in the editors assume-console [ press 65532 # F4 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] # check that screen prints the results screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊divide-with-remainder 11, 3 . . ┊3 . . ┊2 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] screen-should-contain-in-color 7/white, [ . . . . . . . . . divide-with-remainder 11, 3 . . . . . . . . . ] screen-should-contain-in-color 245/grey, [ . . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊ . . ┊3 . . ┊2 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] # run another command assume-console [ left-click 1, 80 type [add 2, 2] press 65532 # F4 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] # check that screen prints the results screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊add 2, 2 . . ┊4 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊divide-with-remainder 11, 3 . . ┊3 . . ┊2 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] ] recipe run-sandboxes [ local-scope env:address:programming-environment-data <- next-ingredient recipes:address:editor-data <- get env:address:programming-environment-data/lookup, recipes:offset current-sandbox:address:editor-data <- get env:address:programming-environment-data/lookup, current-sandbox:offset # copy code from recipe editor, persist, load into mu, save any warnings in:address:array:character <- editor-contents recipes:address:editor-data save [recipes.mu], in:address:array:character recipe-warnings:address:address:array:character <- get-address env:address:programming-environment-data/lookup, recipe-warnings:offset recipe-warnings:address:address:array:character/lookup <- reload in:address:array:character # if recipe editor has errors, stop reply-if recipe-warnings:address:address:array:character/lookup # check contents of right editor (sandbox) { sandbox-contents:address:array:character <- editor-contents current-sandbox:address:editor-data break-unless sandbox-contents:address:array:character # if contents exist, first save them # run them and turn them into a new sandbox-data new-sandbox:address:sandbox-data <- new sandbox-data:type data:address:address:array:character <- get-address new-sandbox:address:sandbox-data/lookup, data:offset data:address:address:array:character/lookup <- copy sandbox-contents:address:array:character # push to head of sandbox list dest:address:address:sandbox-data <- get-address env:address:programming-environment-data/lookup, sandbox:offset next:address:address:sandbox-data <- get-address new-sandbox:address:sandbox-data/lookup, next-sandbox:offset next:address:address:sandbox-data/lookup <- copy dest:address:address:sandbox-data/lookup dest:address:address:sandbox-data/lookup <- copy new-sandbox:address:sandbox-data # clear sandbox editor init:address:address:duplex-list <- get-address current-sandbox:address:editor-data/lookup, data:offset init:address:address:duplex-list/lookup <- push-duplex 167/§, 0/tail } # save all sandboxes before running, just in case we die when running # first clear previous versions, in case we deleted some sandbox $system [rm lesson/[0-9]*] curr:address:sandbox-data <- get env:address:programming-environment-data/lookup, sandbox:offset filename:number <- copy 0 { break-unless curr:address:sandbox-data data:address:address:array:character <- get-address curr:address:sandbox-data/lookup, data:offset save filename:number, data:address:address:array:character/lookup filename:number <- add filename:number, 1 curr:address:sandbox-data <- get curr:address:sandbox-data/lookup, next-sandbox:offset loop } # run all sandboxes curr:address:sandbox-data <- get env:address:programming-environment-data/lookup, sandbox:offset { break-unless curr:address:sandbox-data data:address:address:array:character <- get-address curr:address:sandbox-data/lookup, data:offset response:address:address:array:character <- get-address curr:address:sandbox-data/lookup, response:offset warnings:address:address:array:character <- get-address curr:address:sandbox-data/lookup, warnings:offset fake-screen:address:address:screen <- get-address curr:address:sandbox-data/lookup, screen:offset response:address:address:array:character/lookup, warnings:address:address:array:character/lookup, fake-screen:address:address:screen/lookup <- run-interactive data:address:address:array:character/lookup #? $print warnings:address:address:array:character/lookup, [ ], warnings:address:address:array:character/lookup/lookup, 10/newline curr:address:sandbox-data <- get curr:address:sandbox-data/lookup, next-sandbox:offset loop } ] recipe render-sandbox-side [ local-scope screen:address <- next-ingredient env:address:programming-environment-data <- next-ingredient clear:boolean <- next-ingredient #? trace [app], [render sandbox side] #? 1 current-sandbox:address:editor-data <- get env:address:programming-environment-data/lookup, current-sandbox:offset left:number <- get current-sandbox:address:editor-data/lookup, left:offset right:number <- get current-sandbox:address:editor-data/lookup, right:offset row:number, screen:address <- render screen:address, current-sandbox:address:editor-data row:number <- add row:number, 1 draw-horizontal screen:address, row:number, left:number, right:number, 9473/horizontal-double sandbox:address:sandbox-data <- get env:address:programming-environment-data/lookup, sandbox:offset row:number, screen:address <- render-sandboxes screen:address, sandbox:address:sandbox-data, left:number, right:number, row:number # clear next line, in case we just processed a backspace row:number <- add row:number, 1 move-cursor screen:address, row:number, left:number clear-line-delimited screen:address, left:number, right:number reply-unless clear:boolean, screen:address/same-as-ingredient:0 screen-height:number <- screen-height screen:address { at-bottom-of-screen?:boolean <- greater-or-equal row:number, screen-height:number break-if at-bottom-of-screen?:boolean move-cursor screen:address, row:number, left:number clear-line-delimited screen:address, left:number, right:number row:number <- add row:number, 1 loop } reply screen:address/same-as-ingredient:0 ] recipe render-sandboxes [ local-scope screen:address <- next-ingredient sandbox:address:sandbox-data <- next-ingredient left:number <- next-ingredient right:number <- next-ingredient row:number <- next-ingredient reply-unless sandbox:address:sandbox-data, row:number/same-as-ingredient:4, screen:address/same-as-ingredient:0 screen-height:number <- screen-height screen:address at-bottom?:boolean <- greater-or-equal row:number screen-height:number reply-if at-bottom?:boolean, row:number/same-as-ingredient:4, screen:address/same-as-ingredient:0 #? $print [rendering sandbox ], sandbox:address:sandbox-data, 10/newline # render sandbox menu row:number <- add row:number, 1 move-cursor screen:address, row:number, left:number clear-line-delimited screen:address, left:number, right:number print-character screen:address, 120/x, 245/grey # save menu row so we can detect clicks to it later starting-row:address:number <- get-address sandbox:address:sandbox-data/lookup, starting-row-on-screen:offset starting-row:address:number/lookup <- copy row:number # render sandbox contents sandbox-data:address:array:character <- get sandbox:address:sandbox-data/lookup, data:offset row:number, screen:address <- render-string screen:address, sandbox-data:address:array:character, left:number, right:number, 7/white, row:number # render sandbox warnings, screen or response, in that order sandbox-response:address:array:character <- get sandbox:address:sandbox-data/lookup, response:offset sandbox-warnings:address:array:character <- get sandbox:address:sandbox-data/lookup, warnings:offset sandbox-screen:address <- get sandbox:address:sandbox-data/lookup, screen:offset { break-unless sandbox-warnings:address:array:character row:number, screen:address <- render-string screen:address, sandbox-warnings:address:array:character, left:number, right:number, 1/red, row:number } { break-if sandbox-warnings:address:array:character empty-screen?:boolean <- fake-screen-is-clear? sandbox-screen:address break-if empty-screen?:boolean row:number, screen:address <- render-screen screen:address, sandbox-screen:address, left:number, right:number, row:number } { break-if sandbox-warnings:address:array:character break-unless empty-screen?:boolean row:number, screen:address <- render-string screen:address, sandbox-response:address:array:character, left:number, right:number, 245/grey, row:number } at-bottom?:boolean <- greater-or-equal row:number screen-height:number reply-if at-bottom?:boolean, row:number/same-as-ingredient:4, screen:address/same-as-ingredient:0 # draw solid line after sandbox draw-horizontal screen:address, row:number, left:number, right:number, 9473/horizontal-double # draw next sandbox next-sandbox:address:sandbox-data <- get sandbox:address:sandbox-data/lookup, next-sandbox:offset row:number, screen:address <- render-sandboxes screen:address, next-sandbox:address:sandbox-data, left:number, right:number, row:number reply row:number/same-as-ingredient:4, screen:address/same-as-ingredient:0 ] # assumes programming environment has no sandboxes; restores them from previous session recipe restore-sandboxes [ local-scope env:address:programming-environment-data <- next-ingredient # read all scenarios, pushing them to end of a list of scenarios filename:number <- copy 0 curr:address:address:sandbox-data <- get-address env:address:programming-environment-data/lookup, sandbox:offset { contents:address:array:character <- restore filename:number break-unless contents:address:array:character # stop at first error; assuming file didn't exist #? $print contents:address:array:character, 10/newline # create new sandbox for file curr:address:address:sandbox-data/lookup <- new sandbox-data:type data:address:address:array:character <- get-address curr:address:address:sandbox-data/lookup/lookup, data:offset data:address:address:array:character/lookup <- copy contents:address:array:character # increment loop variables filename:number <- add filename:number, 1 curr:address:address:sandbox-data <- get-address curr:address:address:sandbox-data/lookup/lookup, next-sandbox:offset loop } reply env:address:programming-environment-data/same-as-ingredient:0 ] # was-deleted?:boolean <- delete-sandbox t:touch-event, env:address:programming-environment-data recipe delete-sandbox [ local-scope t:touch-event <- next-ingredient env:address:programming-environment-data <- next-ingredient click-column:number <- get t:touch-event, column:offset current-sandbox:address:editor-data <- get env:address:programming-environment-data/lookup, current-sandbox:offset right:number <- get current-sandbox:address:editor-data/lookup, right:offset #? $print [comparing column ], click-column:number, [ vs ], right:number, 10/newline at-right?:boolean <- equal click-column:number, right:number reply-unless at-right?:boolean, 0/false #? $print [trying to delete #? ] #? 1 click-row:number <- get t:touch-event, row:offset prev:address:address:sandbox-data <- get-address env:address:programming-environment-data/lookup, sandbox:offset #? $print [prev: ], prev:address:address:sandbox-data, [ -> ], prev:address:address:sandbox-data/lookup, 10/newline curr:address:sandbox-data <- get env:address:programming-environment-data/lookup, sandbox:offset { #? $print [next sandbox #? ] #? 1 break-unless curr:address:sandbox-data # more sandboxes to check { #? $print [checking #? ] #? 1 target-row:number <- get curr:address:sandbox-data/lookup, starting-row-on-screen:offset #? $print [comparing row ], target-row:number, [ vs ], click-row:number, 10/newline delete-curr?:boolean <- equal target-row:number, click-row:number break-unless delete-curr?:boolean #? $print [found! #? ] #? 1 # delete this sandbox, rerender and stop prev:address:address:sandbox-data/lookup <- get curr:address:sandbox-data/lookup, next-sandbox:offset #? $print [setting prev: ], prev:address:address:sandbox-data, [ -> ], prev:address:address:sandbox-data/lookup, 10/newline reply 1/true } prev:address:address:sandbox-data <- get-address curr:address:sandbox-data/lookup, next-sandbox:offset #? $print [prev: ], prev:address:address:sandbox-data, [ -> ], prev:address:address:sandbox-data/lookup, 10/newline curr:address:sandbox-data <- get curr:address:sandbox-data/lookup, next-sandbox:offset loop } reply 0/false ] scenario run-updates-results [ $close-trace # trace too long for github assume-screen 100/width, 12/height # define a recipe (no indent for the 'add' line below so column numbers are more obvious) 1:address:array:character <- new [ recipe foo [ z:number <- add 2, 2 ]] # sandbox editor contains an instruction without storing outputs 2:address:array:character <- new [foo] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # run the code in the editors assume-console [ press 65532 # F4 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] # check that screen prints the results screen-should-contain [ . run (F4) . . ┊ . .recipe foo [ ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. .z:number <- add 2, 2 ┊ x. .] ┊foo . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊4 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] # make a change (incrementing one of the args to 'add'), then rerun assume-console [ left-click 3, 28 # one past the value of the second arg type [«3] # replace press 65532 # F4 ] 4:event/backspace <- merge 0/text, 8/backspace, 0/dummy, 0/dummy replace-in-console 171/«, 4:event/backspace run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] # check that screen updates the result on the right screen-should-contain [ . run (F4) . . ┊ . .recipe foo [ ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. .z:number <- add 2, 3 ┊ x. .] ┊foo . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊5 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] ] scenario run-instruction-and-print-warnings [ $close-trace # trace too long for github assume-screen 100/width, 10/height # left editor is empty 1:address:array:character <- new [] # right editor contains an illegal instruction 2:address:array:character <- new [get 1234:number, foo:offset] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # run the code in the editors assume-console [ press 65532 # F4 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] # check that screen prints error message in red screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊get 1234:number, foo:offset . . ┊unknown element foo in container number . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] screen-should-contain-in-color 7/white, [ . . . . . . . . . get 1234:number, foo:offset . . . . . . . ] screen-should-contain-in-color 1/red, [ . . . . . . . . . . . unknown element foo in container number . . . ] screen-should-contain-in-color 245/grey, [ . . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊ . . ┊ . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] ] scenario run-instruction-and-print-warnings-only-once [ $close-trace # trace too long for github assume-screen 100/width, 10/height # left editor is empty 1:address:array:character <- new [] # right editor contains an illegal instruction 2:address:array:character <- new [get 1234:number, foo:offset] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # run the code in the editors multiple times assume-console [ press 65532 # F4 press 65532 # F4 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] # check that screen prints error message just once screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊get 1234:number, foo:offset . . ┊unknown element foo in container number . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] ] scenario deleting-sandboxes [ $close-trace # trace too long for github assume-screen 100/width, 15/height 1:address:array:character <- new [] 2:address:array:character <- new [] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # run a few commands assume-console [ left-click 1, 80 type [divide-with-remainder 11, 3] press 65532 # F4 type [add 2, 2] press 65532 # F4 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊add 2, 2 . . ┊4 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊divide-with-remainder 11, 3 . . ┊3 . . ┊2 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] # delete second sandbox assume-console [ left-click 7, 99 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊add 2, 2 . . ┊4 . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . . ┊ . ] # delete first sandbox assume-console [ left-click 3, 99 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . . ┊ . ] ] scenario run-instruction-manages-screen-per-sandbox [ $close-trace # trace too long for github #? 1 assume-screen 100/width, 20/height # left editor is empty 1:address:array:character <- new [] # right editor contains an illegal instruction 2:address:array:character <- new [print-integer screen:address, 4] 3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character # run the code in the editor assume-console [ press 65532 # F4 ] run [ event-loop screen:address, console:address, 3:address:programming-environment-data ] # check that it prints a little 5x5 toy screen # hack: screen address is brittle screen-should-contain [ . run (F4) . . ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ x. . ┊print-integer screen:address, 4 . . ┊screen: . . ┊ .4 . . . ┊ . . . . ┊ . . . . ┊ . . . . ┊ . . . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] ] recipe editor-contents [ local-scope editor:address:editor-data <- next-ingredient buf:address:buffer <- new-buffer 80 curr:address:duplex-list <- get editor:address:editor-data/lookup, data:offset # skip § sentinel assert curr:address:duplex-list, [editor without data is illegal; must have at least a sentinel] curr:address:duplex-list <- next-duplex curr:address:duplex-list reply-unless curr:address:duplex-list, 0 { break-unless curr:address:duplex-list c:character <- get curr:address:duplex-list/lookup, value:offset buffer-append buf:address:buffer, c:character curr:address:duplex-list <- next-duplex curr:address:duplex-list loop } result:address:array:character <- buffer-to-array buf:address:buffer reply result:address:array:character ] scenario editor-provides-edited-contents [ 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 [ left-click 1, 2 type [def] ] run [ editor-event-loop screen:address, console:address, 2:address:editor-data 3:address:array:character <- editor-contents 2:address:editor-data 4:array:character <- copy 3:address:array:character/lookup ] memory-should-contain [ 4:string <- [abdefc] ] ] ## handling malformed programs scenario run-shows-warnings-in-get [ $close-trace assume-screen 100/width, 15/height assume-console [ press 65532 # F4 ] run [ x:address:array:character <- new [ recipe foo [ get 123:number, foo:offset ]] y:address:array:character <- new [foo] env:address:programming-environment-data <- new-programming-environment screen:address, x:address:array:character, y:address:array:character event-loop screen:address, console:address, env:address:programming-environment-data ] screen-should-contain [ . run (F4) . . ┊foo . .recipe foo [ ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . get 123:number, foo:offset ┊ . .] ┊ . .unknown element foo in container number ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . ┊ . ] screen-should-contain-in-color 1/red, [ . . . . . . . . . . .unknown element foo in container number . . . ] ] scenario run-shows-missing-type-warnings [ $close-trace assume-screen 100/width, 15/height assume-console [ press 65532 # F4 ] run [ x:address:array:character <- new [ recipe foo [ x:number <- copy 0 copy x ]] y:address:array:character <- new [foo] env:address:programming-environment-data <- new-programming-environment screen:address, x:address:array:character, y:address:array:character event-loop screen:address, console:address, env:address:programming-environment-data ] screen-should-contain [ . run (F4) . . ┊foo . .recipe foo [ ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . x:number <- copy 0 ┊ . . copy x ┊ . .] ┊ . .missing type in 'copy x' ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . ┊ . ] ] scenario run-shows-get-on-non-container-warnings [ $close-trace assume-screen 100/width, 15/height assume-console [ press 65532 # F4 ] run [ x:address:array:character <- new [ recipe foo [ x:address:point <- new point:type get x:address:point, 1:offset ]] y:address:array:character <- new [foo] env:address:programming-environment-data <- new-programming-environment screen:address, x:address:array:character, y:address:array:character event-loop screen:address, console:address, env:address:programming-environment-data ] screen-should-contain [ . run (F4) . . ┊ . .recipe foo [ ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . x:address:point <- new point:type ┊ x. . get x:address:point, 1:offset ┊foo . .] ┊foo: first ingredient of 'get' should be a conta↩. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊iner, but got x:address:point . . ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . ┊ . ] ] scenario run-shows-non-literal-get-argument-warnings [ $close-trace assume-screen 100/width, 15/height assume-console [ press 65532 # F4 ] run [ x:address:array:character <- new [ recipe foo [ x:number <- copy 0 y:address:point <- new point:type get y:address:point/lookup, x:number ]] y:address:array:character <- new [foo] env:address:programming-environment-data <- new-programming-environment screen:address, x:address:array:character, y:address:array:character event-loop screen:address, console:address, env:address:programming-environment-data ] screen-should-contain [ . run (F4) . . ┊foo . .recipe foo [ ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━. . x:number <- copy 0 ┊ . . y:address:point <- new point:type ┊ . . get y:address:point/lookup, x:number ┊ . .] ┊ . .foo: expected ingredient 1 of 'get' to have type ↩┊ . .'offset'; got x:number ┊ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . ┊ . ] ] ## helpers for drawing editor borders recipe draw-box [ local-scope screen:address <- next-ingredient top:number <- next-ingredient left:number <- next-ingredient bottom:number <- next-ingredient right:number <- next-ingredient color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found?:boolean color:number <- copy 245/grey } # top border draw-horizontal screen:address, top:number, left:number, right:number, color:number draw-horizontal screen:address, bottom:number, left:number, right:number, color:number draw-vertical screen:address, left:number, top:number, bottom:number, color:number draw-vertical screen:address, right:number, top:number, bottom:number, color:number draw-top-left screen:address, top:number, left:number, color:number draw-top-right screen:address, top:number, right:number, color:number draw-bottom-left screen:address, bottom:number, left:number, color:number draw-bottom-right screen:address, bottom:number, right:number, color:number # position cursor inside box move-cursor screen:address, top:number, left:number cursor-down screen:address cursor-right screen:address ] recipe draw-horizontal [ local-scope screen:address <- next-ingredient row:number <- next-ingredient x:number <- next-ingredient right:number <- next-ingredient style:character, style-found?:boolean <- next-ingredient { break-if style-found?:boolean style:character <- copy 9472/horizontal } color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found?:boolean color:number <- copy 245/grey } bg-color:number, bg-color-found?:boolean <- next-ingredient { break-if bg-color-found?:boolean bg-color:number <- copy 0/black } move-cursor screen:address, row:number, x:number { continue?:boolean <- lesser-or-equal x:number, right:number # right is inclusive, to match editor-data semantics break-unless continue?:boolean print-character screen:address, style:character, color:number, bg-color:number x:number <- add x:number, 1 loop } ] recipe draw-vertical [ local-scope screen:address <- next-ingredient col:number <- next-ingredient x:number <- next-ingredient bottom:number <- next-ingredient style:character, style-found?:boolean <- next-ingredient { break-if style-found?:boolean style:character <- copy 9474/vertical } color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found?:boolean color:number <- copy 245/grey } { continue?:boolean <- lesser-than x:number, bottom:number break-unless continue?:boolean move-cursor screen:address, x:number, col:number print-character screen:address, style:character, color:number x:number <- add x:number, 1 loop } ] recipe draw-top-left [ local-scope screen:address <- next-ingredient top:number <- next-ingredient left:number <- next-ingredient color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found?:boolean color:number <- copy 245/grey } move-cursor screen:address, top:number, left:number print-character screen:address, 9484/down-right, color:number ] recipe draw-top-right [ local-scope screen:address <- next-ingredient top:number <- next-ingredient right:number <- next-ingredient color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found?:boolean color:number <- copy 245/grey } move-cursor screen:address, top:number, right:number print-character screen:address, 9488/down-left, color:number ] recipe draw-bottom-left [ local-scope screen:address <- next-ingredient bottom:number <- next-ingredient left:number <- next-ingredient color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found?:boolean color:number <- copy 245/grey } move-cursor screen:address, bottom:number, left:number print-character screen:address, 9492/up-right, color:number ] recipe draw-bottom-right [ local-scope screen:address <- next-ingredient bottom:number <- next-ingredient right:number <- next-ingredient color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found?:boolean color:number <- copy 245/grey } move-cursor screen:address, bottom:number, right:number print-character screen:address, 9496/up-left, color:number ] recipe print-string-with-gradient-background [ local-scope x:address:screen <- next-ingredient s:address:array:character <- next-ingredient color:number <- next-ingredient bg-color1:number <- next-ingredient bg-color2:number <- next-ingredient len:number <- length s:address:array:character/lookup color-range:number <- subtract bg-color2:number, bg-color1:number color-quantum:number <- divide color-range:number, len:number #? close-console #? 2 #? $print len:number, [, ], color-range:number, [, ], color-quantum:number, 10/newline #? #? $exit #? 3 bg-color:number <- copy bg-color1:number i:number <- copy 0 { done?:boolean <- greater-or-equal i:number, len:number break-if done?:boolean c:character <- index s:address:array:character/lookup, i:number print-character x:address:screen, c:character, color:number, bg-color:number i:number <- add i:number, 1 bg-color:number <- add bg-color:number, color-quantum:number #? $print [=> ], bg-color:number, 10/newline loop } #? $exit #? 1 reply x:address:screen/same-as-ingredient:0 ]