# 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
]