about summary refs log blame commit diff stats
path: root/edit.mu
blob: 40576e3c7696ef35263d353c0896fc2c5eec4d1e (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                                                                                
 
             
                                                                       
              

                                 
               

                                                                              

                                           
                                                                                                                                                                    













                                                                                                                                              
               

 
                                                   
                                                  
       

                                                                                                         




                         

 


                                                                            
                       
                                                                       


                                                          

                                      









                                        
 

                                                                          
                                 
                                                                                  

 




                                                                                                                                


                                                                       




                                                  
                                                    








                                                                                          
                                        





                                                                                        

                           
                                            





                                                                                                        



                                                                                     

                                                   
                            



                                                                    
                                                               
                                                                         
   

                                                

                                                        


                                                                    

                                     



                                                                    
        
   
                                      
                                                                                                     


                                                                                      
                                                                 


                                  






                                                                                                                      


                                          












                                


               
                                                                       
                                               
                       
         


                                                                       
                                                      
                                                                    
                            

                                                                                        
                                                           
                                                                  
                     
                               
                                   


                                                                                                                 


                                                       


                                          
                                                                            
                                


                                                                              
     
                                                                                 
                                         
                                                                                   
                                     
                                                                                             


                                                                                                                                                             
     
                                                                   
                                           
         



                                                               







                                                                                                


                                                                                                                                                               
       











                                                                     
                         


                                                           
                                                                      
                                                                      

                                



                                                             


                                                                             






                                                                                      
                            

                                
                                               
                                                                    
                                                                    


                                                 


                                                                                      


                                                                               

                                                                                                     


                                                                                               

                                     

                                                            

                                                                                                   
                                                                                    


                                                                                                                                                            
   









                                                                              
                 




                                                                                                   
                            
                                                       
 
 
                                                 
                                                 


                                         
                                                                                                        







                         
                                           
                                                 

                                          
                                                                                                        






                         
 
                                                           
                                                 


                                         
                                                                                                        






                         
 
                                            
                                                 

                                              
                                                                                                        











                                                    
 


















                                                                                                                                 
                                                              
 

                                                                              


                                                                       
                                    

                                               
                                    
                                                                                         
                              
                                           
                             


                                                               
     



                                                                                        
     

                                                               
     



                                                                                        
     













                                                                                        




                     
                                                                       






















                                                                                             
                      


                                                              






                                                                                





















                                                                                                                 
     



                                                                                       













                                                                                                       




                                                                                     






                                                                                                       
           
     










                                                                                           
     



                                                                                                  
     














                                                                                                                       
                                                                                          

                                                                                                      


   


                                                                       
                                  


                                                                                             


                                                                       
                                

                                                                          

                                                           
         

                                                        




                                                                                                    

                                                              





                                                                       

                                      

                                                                                                                 
                                                                            


                                                                                                    




                                                                
                                                                                     
                                                        
         
   




















                                                                                                         
                               
                                                                                         

 
















                                                                                                                 

                                                                             


















                                                                                  


                     

                                                  






                                                                                                                                
                   

                                     
       
                                                                     
   

                                     







                                                  

                                                                                                                                
                  
                                 

       
                                                                     












                                                                     

                                                   

                                                                                                                                
                  
                                                      

       











                                                                     


                                                                                                                                
                  
                                                          

       











                                                                     


                                                                                                                                
                  
                                 

       









                                                                     

                                                     


                                                                                                                               




                                   













                                                                     

                                                      

                                                                                                                               



                  







                                                                     
                                              
                                                  

                                                                                                                                





                   







                                                                     

                                                  

                                                                                                                                




                                         









                                                                     

                                                                                                                                




                                     









                                                                     


                                                                                                                                




                                     








                                                                     

                                                  


                                                                                                                                




                                                           








                                                                     

                                                 


                                                                                                                               



                  

                                                                     
               
                         


           
                        





                                                                     
            
                         





             

                                                         

                                                                                                                               



                  







                                                                     















































                                                                                                                               

                                                           

                                                                                                                                




                  








                                                                     




























                                                                                                                                

                                                  

                                                                                                                                




                              







                                                                     

                                                            


                                                                                                                                







                                          








                                                                     























                                                                                                                               


















                                                                                                                               

                                                                           


                                                                                                                                





                                          








                                                                     

                                                  

                                                                                                                                





                             







                                                                     

                                                                                



                                                                                                                                






                                                                            










                                                                                  






                                                                                                                                





                             











                                                                                  



                                                                                                                                






                                                   











                                                                                  




                                                                                                                                






                                          









                                                                     

























                                                                                                                               
























                                                                                                                                              
                                                  




























                                                                                                                                              












































                                                                                                                                              





















                                                                                            
                                     
 






                                                                       





                                                       
              







                                                                                        
                              
                                                     









                                                                       





                                                       
                                                  


                                                           
                                                                         


                                       







                                                                       





                                                       


                                                            
                                                    
                                                                       


                                       






                                                                       





                                                       
                                                     
                                                                       






                                                                       





                                                       
                                                      
                                                                      






                                                                       





                                                       
                                                        
                                                                     






                                                                       





                                                       
                                                         
                                                                    
 
# Editor widget: takes a string and screen coordinates, modifying them in place.

recipe main [
  default-space:address:array:location <- new location:type, 30:literal
  open-console
  width:number <- display-width
  height:number <- display-height
  # draw a line
  divider:number, _ <- divide-with-remainder width:number, 2:literal
  draw-vertical 0:literal/screen, divider:number, 0:literal/top, height:number
  # editor on the left
  left:address:array:character <- new [abc]
  left-editor:address:editor-data <- new-editor left:address:array:character, 0:literal/screen, 0:literal/top, 0:literal/left, 5:literal/right #divider:number/right
  # editor on the right
  right:address:array:character <- new [def]
  new-left:number <- add divider:number/right, 1:literal
  right-editor:address:editor-data <- new-editor right:address:array:character, 0:literal/screen, 0:literal/top, new-left:number, width:number
  # chain
  x:address:address:editor-data <- get-address left-editor:address:editor-data/deref, next-editor:offset
  x:address:address:editor-data/deref <- copy right-editor:address:editor-data
  # initialize focus
  reset-focus left-editor:address:editor-data
  cursor-row:number <- get left-editor:address:editor-data/deref, cursor-row:offset
  cursor-column:number <- get left-editor:address:editor-data/deref, cursor-column:offset
  move-cursor 0:literal/screen, cursor-row:number, cursor-column:number
  # and we're off!
  event-loop 0:literal/screen, 0:literal/events, left-editor:address:editor-data
  close-console
]

scenario editor-initially-prints-string-to-screen [
  assume-screen 10:literal/width, 5:literal/height
  run [
    1:address:array:character <- new [abc]
    new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/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 [
  # doubly linked list of characters (head contains a special sentinel)
  data:address:duplex-list
  # location of top-left of screen inside data (scrolling)
  top-of-screen:address:duplex-list
  # location before cursor inside data
  before-cursor:address:duplex-list

  screen:address:screen
  # raw bounds of display area on screen
  top:number
  left:number
  bottom:number
  right:number
  # raw screen coordinates of cursor
  cursor-row:number
  cursor-column:number

  # pointer to another editor, responsible for a different area of screen.
  # helps organize editors in a 'chain'.
  next-editor:address:editor-data
  in-focus?:boolean  # set for the one editor in this chain currently being edited
]

# editor:address, screen:address <- new-editor s:address:array:character, screen:address, top:number, left:number, bottom: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 [
  default-space:address:array:location <- new location:type, 30:literal
  s:address:array:character <- next-ingredient
  screen:address <- next-ingredient
  # no clipping of bounds
  top:number <- next-ingredient
  left:number <- next-ingredient
  right:number <- next-ingredient
  right:number <- subtract right:number, 1:literal
  result:address:editor-data <- new editor-data:type
  # initialize screen-related fields
  sc:address:address:screen <- get-address result:address:editor-data/deref, screen:offset
  sc:address:address:screen/deref <- copy screen:address
  x:address:number <- get-address result:address:editor-data/deref, top:offset
  x:address:number/deref <- copy top:number
  x:address:number <- get-address result:address:editor-data/deref, left:offset
  x:address:number/deref <- copy left:number
  x:address:number <- get-address result:address:editor-data/deref, right:offset
  x:address:number/deref <- copy right:number
  # bottom = top (in case of early exit)
  x:address:number <- get-address result:address:editor-data/deref, bottom:offset
  x:address:number/deref <- copy top:number
  # initialize cursor
  x:address:number <- get-address result:address:editor-data/deref, cursor-row:offset
  x:address:number/deref <- copy top:number
  x:address:number <- get-address result:address:editor-data/deref, cursor-column:offset
#?   $print left:number, [ 
#? ] #? 1
  x:address:number/deref <- copy left:number
  d:address:address:duplex-list <- get-address result:address:editor-data/deref, data:offset
  d:address:address:duplex-list/deref <- push-duplex 167:literal/§, 0:literal/tail
  y:address:address:duplex-list <- get-address result:address:editor-data/deref, before-cursor:offset
  y:address:address:duplex-list/deref <- copy d:address:address:duplex-list/deref
  init:address:address:duplex-list <- get-address result:address:editor-data/deref, top-of-screen:offset
  init:address:address:duplex-list/deref <- copy d:address:address:duplex-list/deref
  # set focus
  # if using multiple editors, must call reset-focus after chaining them all
  b:address:boolean <- get-address result:address:editor-data/deref, in-focus?:offset
  b:address:boolean/deref <- copy 1:literal/true
#?   $print d:address:address:duplex-list/deref, [ 
#? ] #? 1
  # early exit if s is empty
  reply-unless s:address:array:character, result:address:editor-data
  len:number <- length s:address:array:character/deref
  reply-unless len:number, result:address:editor-data
  idx:number <- copy 0:literal
  # now we can start appending the rest, character by character
  curr:address:duplex-list <- copy init:address:address:duplex-list/deref
  {
#?     $print idx:number, [ vs ], len:number, [ 
#? ] #? 1
#?     $print [append to ], curr:address:duplex-list, [ 
#? ] #? 1
    done?:boolean <- greater-or-equal idx:number, len:number
    break-if done?:boolean
    c:character <- index s:address:array:character/deref, idx:number
#?     $print [aa: ], c:character, [ 
#? ] #? 1
    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:literal
    loop
  }
  # initialize cursor to top of screen
  y:address:address:duplex-list <- get-address result:address:editor-data/deref, before-cursor:offset
  y:address:address:duplex-list/deref <- copy init:address:address:duplex-list/deref
  # perform initial rendering to screen
  bottom:address:number <- get-address result:address:editor-data/deref, bottom:offset
  result:address:editor-data <- render result:address:editor-data
  reply result:address:editor-data
]

scenario editor-initializes-without-data [
  assume-screen 5:literal/width, 3:literal/height
  run [
    1:address:editor-data <- new-editor 0:literal/data, screen:address, 1:literal/top, 2:literal/left, 5:literal/right
    2:editor-data <- copy 1:address:editor-data/deref
  ]
  memory-should-contain [
    # 2 <- just the § sentinel
    # 3 (top of screen) <- the § sentinel
    # 4 (before cursor) <- the § sentinel
    # 5 <- screen
    6 <- 1  # top
    7 <- 2  # left
    8 <- 1  # bottom
    9 <- 4  # right  (inclusive)
    10 <- 1  # cursor row
    11 <- 2  # cursor column
  ]
  screen-should-contain [
    .     .
    .     .
    .     .
  ]
]

recipe render [
  default-space:address:array:location <- new location:type, 40:literal
  editor:address:editor-data <- next-ingredient
#?   $print [=== render
#? ] #? 2
  screen:address <- get editor:address:editor-data/deref, screen:offset
  top:number <- get editor:address:editor-data/deref, top:offset
  left:number <- get editor:address:editor-data/deref, left:offset
  screen-height:number <- screen-height screen:address
  right:number <- get editor:address:editor-data/deref, right:offset
  hide-screen screen:address
  # traversing editor
  curr:address:duplex-list <- get editor:address:editor-data/deref, top-of-screen: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 top:number
  column:number <- copy left:number
  cursor-row:address:number <- get-address editor:address:editor-data/deref, cursor-row:offset
  cursor-column:address:number <- get-address editor:address:editor-data/deref, cursor-column:offset
  before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/deref, before-cursor:offset
  move-cursor screen:address, row:number, column:number
  {
    +next-character
#?     $print curr:address:duplex-list, [ 
#? ] #? 1
    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/deref
      break-unless at-cursor-row?:boolean
      at-cursor?:boolean <- equal column:number, cursor-column:address:number/deref
      break-unless at-cursor?:boolean
      before-cursor:address:address:duplex-list/deref <- prev-duplex curr:address:duplex-list
#?       new-prev:character <- get before-cursor:address:address:duplex-list/deref/deref, value:offset #? 1
#?       $print [render 0: cursor adjusted to after ], new-prev:character, [(], cursor-row:address:number/deref, [, ], cursor-column:address:number/deref, [)
#? ] #? 1
    }
    c:character <- get curr:address:duplex-list/deref, value:offset
#?     $print [rendering ], c:character, [ 
#? ] #? 2
    {
      # newline? move to left rather than 0
      newline?:boolean <- equal c:character, 10:literal/newline
      break-unless newline?:boolean
      # adjust cursor if necessary
      {
        at-cursor-row?:boolean <- equal row:number, cursor-row:address:number/deref
        break-unless at-cursor-row?:boolean
        left-of-cursor?:boolean <- lesser-than column:number, cursor-column:address:number/deref
        break-unless left-of-cursor?:boolean
        cursor-column:address:number/deref <- copy column:number
        before-cursor:address:address:duplex-list/deref <- prev-duplex curr:address:duplex-list
#?         new-prev:character <- get before-cursor:address:address:duplex-list/deref/deref, value:offset #? 1
#?         $print [render 1: cursor adjusted to after ], new-prev:character, [(], cursor-row:address:number/deref, [, ], cursor-column:address:number/deref, [)
#? ] #? 1
      }
      # clear rest of line in this window
#?       $print row:number, [ ], column:number, [ ], right:number, [ 
#? ] #? 1
      {
        done?:boolean <- greater-or-equal column:number, right:number
        break-if done?:boolean
        print-character screen:address, 32:literal/space
        column:number <- add column:number, 1:literal
#?         $print column:number, [ 
#? ] #? 1
        loop
      }
      # skip to next line
      row:number <- add row:number, 1:literal
      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? more than one letter left in the line? wrap
      at-right?:boolean <- equal column:number, right:number
      break-unless at-right?:boolean
      next-node:address:duplex-list <- next-duplex curr:address:duplex-list
      break-unless next-node:address:duplex-list
      next:character <- get next-node:address:duplex-list/deref, value:offset
      next-character-is-newline?:boolean <- equal next:character, 10:literal/newline
      break-if next-character-is-newline?:boolean
      # wrap
      print-character screen:address, 8617:literal/loop-back-to-left, 245:literal/grey
      column:number <- copy left:number
      row:number <- add row:number, 1:literal
      move-cursor screen:address, row:number, column:number
      # don't increment curr
      loop +next-character:label
    }
    print-character screen:address, c:character
    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:literal
    loop
  }
  # bottom = row
  bottom:address:number <- get-address editor:address:editor-data/deref, bottom:offset
  bottom:address:number/deref <- copy row:number
  # is cursor to the right of the last line? move to end
  {
    at-cursor-row?:boolean <- equal row:number, cursor-row:address:number/deref
    cursor-outside-line?:boolean <- lesser-or-equal column:number, cursor-column:address:number/deref
    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/deref
    before-cursor?:boolean <- or before-cursor-on-same-line?:boolean, above-cursor-row?:boolean
    break-unless before-cursor?:boolean
#?     $print [pointed after all text
#? ] #? 1
    cursor-row:address:number/deref <- copy row:number
    cursor-column:address:number/deref <- copy column:number
#?     $print [now ], cursor-row:address:number/deref, [, ], cursor-column:address:number/deref, [ 
#? ] #? 1
    before-cursor:address:address:duplex-list/deref <- copy prev:address:duplex-list
#?     new-prev:character <- get before-cursor:address:address:duplex-list/deref/deref, value:offset #? 1
#?     $print [render Ω: cursor adjusted to after ], new-prev:character, [(], cursor-row:address:number/deref, [, ], cursor-column:address:number/deref, [)
#? ] #? 1
  }
  # clear rest of final line
#?   $print [clearing ], row:number, [ ], column:number, [ ], right:number, [ 
#? ] #? 2
  {
    done?:boolean <- greater-or-equal column:number, right:number
    break-if done?:boolean
    print-character screen:address, 32:literal/space
    column:number <- add column:number, 1:literal
    loop
  }
  # update cursor
  {
    in-focus?:boolean <- get editor:address:editor-data/deref, in-focus?:offset
    break-unless in-focus?:boolean
    move-cursor screen:address, cursor-row:address:number/deref, cursor-column:address:number/deref
  }
  show-screen screen:address
  reply editor:address:editor-data/same-as-ingredient:0
]

scenario editor-initially-prints-multiple-lines [
  assume-screen 5:literal/width, 3:literal/height
  run [
    s:address:array:character <- new [abc
def]
    new-editor s:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  ]
  screen-should-contain [
    .abc  .
    .def  .
    .     .
  ]
]

scenario editor-initially-handles-offsets [
  assume-screen 5:literal/width, 3:literal/height
  run [
    s:address:array:character <- new [abc]
    new-editor s:address:array:character, screen:address, 0:literal/top, 1:literal/left, 5:literal/right
  ]
  screen-should-contain [
    . abc .
    .     .
    .     .
  ]
]

scenario editor-initially-prints-multiple-lines-at-offset [
  assume-screen 5:literal/width, 3:literal/height
  run [
    s:address:array:character <- new [abc
def]
    new-editor s:address:array:character, screen:address, 0:literal/top, 1:literal/left, 5:literal/right
  ]
  screen-should-contain [
    . abc .
    . def .
    .     .
  ]
]

scenario editor-initially-wraps-long-lines [
  assume-screen 5:literal/width, 3:literal/height
  run [
    s:address:array:character <- new [abc def]
    new-editor s:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  ]
  screen-should-contain [
    .abc .
    .def  .
    .     .
  ]
  screen-should-contain-in-color, 245:literal/grey [
    .    .
    .     .
    .     .
  ]
]

scenario editor-initializes-empty-text [
  assume-screen 5:literal/width, 3:literal/height
  run [
    1:address:array:character <- new []
    2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  screen-should-contain [
    .     .
    .     .
    .     .
  ]
  memory-should-contain [
    3 <- 0  # cursor row
    4 <- 0  # cursor column
  ]
]

## handling events from the keyboard, mouse, touch screen, ...

# Takes a chain of editors (chained using editor-data.next-editor), sends each
# event from the console to each editor.
recipe event-loop [
  default-space:address:array:location <- new location:type, 30:literal
  screen:address <- next-ingredient
  console:address <- next-ingredient
  editor:address:editor-data <- next-ingredient
  {
    # send each event to each editor
    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]
#?     $print [--- new event
#? ] #? 1
    curr:address:editor-data <- copy editor:address:editor-data
    {
      break-unless curr:address:editor-data
      handle-event screen:address, console:address, curr:address:editor-data, e:event
      curr:address:editor-data <- get curr:address:editor-data/deref, next-editor:offset
      loop
    }
    # after each non-trivial event, render all editors
    curr:address:editor-data <- copy editor:address:editor-data
    {
      break-unless curr:address:editor-data
      render curr:address:editor-data
      curr:address:editor-data <- get curr:address:editor-data/deref, next-editor:offset
      loop
    }
    # ..and position the cursor
    curr:address:editor-data <- copy editor:address:editor-data
    {
      break-unless curr:address:editor-data
      {
        in-focus?:boolean <- get curr:address:editor-data/deref, in-focus?:offset
        break-unless in-focus?:boolean
        cursor-row:number <- get curr:address:editor-data/deref, cursor-row:offset
        cursor-column:number <- get curr:address:editor-data/deref, cursor-column:offset
        move-cursor screen:address, cursor-row:number, cursor-column:number
      }
      curr:address:editor-data <- get curr:address:editor-data/deref, next-editor:offset
      loop
    }
    loop
  }
]

recipe handle-event [
  default-space:address:array:location <- new location:type, 40:literal
  screen:address <- next-ingredient
  console:address <- next-ingredient
  editor:address:editor-data <- next-ingredient
  e:event <- next-ingredient
  # 'touch' event
  {
    t:address:touch-event <- maybe-convert e:event, touch:variant
    break-unless t:address:touch-event
    move-cursor-in-editor editor:address:editor-data, t:address:touch-event/deref
    reply
  }
  # other events trigger only if this editor is in focus
#?   $print [checking ], editor:address:editor-data, [ 
#? ] #? 1
#?   x:address:boolean <- get-address editor:address:editor-data/deref, in-focus?:offset #? 1
#?   $print [address of focus: ], x:address:boolean, [ 
#? ] #? 1
  in-focus?:address:boolean <- get-address editor:address:editor-data/deref, in-focus?:offset
#?   $print [ at ], in-focus?:address:boolean, [ 
#? ] #? 1
  reply-unless in-focus?:address:boolean/deref
#?   $print [in focus: ], editor:address:editor-data, [ 
#? ] #? 1
  # typing a character
  {
    c:address:character <- maybe-convert e:event, text:variant
    break-unless c:address:character
    # unless it's a backspace
    {
      backspace?:boolean <- equal c:address:character/deref, 8:literal/backspace
      break-unless backspace?:boolean
      delete-before-cursor editor:address:editor-data
      reply
    }
    insert-at-cursor editor:address:editor-data, c:address:character/deref
    reply
  }
  # otherwise it's a special key to control the editor
  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/deref, data:offset
  before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/deref, before-cursor:offset
  cursor-row:address:number <- get-address editor:address:editor-data/deref, cursor-row:offset
  cursor-column:address:number <- get-address editor:address:editor-data/deref, cursor-column:offset
  # arrows; update cursor-row and cursor-column, leave before-cursor to 'render'
  # right arrow
  {
    next-character?:boolean <- equal k:address:number/deref, 65514:literal/right-arrow
    break-unless next-character?:boolean
    # if not at end of text
    next:address:duplex-list <- next-duplex before-cursor:address:address:duplex-list/deref
    break-unless next:address:duplex-list
    # scan to next character
    before-cursor:address:address:duplex-list/deref <- copy next:address:duplex-list
    nextc:character <- get before-cursor:address:address:duplex-list/deref/deref, value:offset
    # if it's a newline, move cursor to start of next row
    {
      at-newline?:boolean <- equal nextc:character, 10:literal/newline
      break-unless at-newline?:boolean
      cursor-row:address:number/deref <- add cursor-row:address:number/deref, 1:literal
      cursor-column:address:number/deref <- copy 0:literal
      # 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/deref, 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
      right:number <- get editor:address:editor-data/deref, right:offset
      wrap-column:number <- subtract right:number, 1:literal
      at-wrap?:boolean <- equal cursor-column:address:number/deref, wrap-column:number
      break-unless at-wrap?:boolean
      # and if character after next isn't newline
#?       $print [aaa] #? 1
      next-next:address:duplex-list <- next-duplex next:address:duplex-list
      break-unless next-next:address:duplex-list
      next-next-next:address:duplex-list <- next-duplex next-next:address:duplex-list
      break-unless next-next-next:address:duplex-list
      # TODO
      cursor-row:address:number/deref <- add cursor-row:address:number/deref, 1:literal
      cursor-column:address:number/deref <- copy 0:literal
      # 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/deref, 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/deref <- add cursor-column:address:number/deref, 1:literal
  }
  # left arrow
  {
    prev-character?:boolean <- equal k:address:number/deref, 65515:literal/left-arrow
    break-unless prev-character?:boolean
    # if not at start of text
    prev:address:duplex-list <- prev-duplex before-cursor:address:address:duplex-list/deref
    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/deref, 0:literal
      break-if at-left-margin?:boolean
      cursor-column:address:number/deref <- subtract cursor-column:address:number/deref, 1:literal
      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/deref/deref, value:offset
      previous-character-is-newline?:boolean <- equal prevc:character, 10:literal/newline
      break-unless previous-character-is-newline?:boolean
      # compute length of previous line
      end-of-line:number <- previous-line-length before-cursor:address:address:duplex-list/deref, d:address:duplex-list
      cursor-row:address:number/deref <- subtract cursor-row:address:number/deref, 1:literal
      cursor-column:address:number/deref <- 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/deref, [unimplemented: moving cursor above top of screen]
    cursor-row:address:number/deref <- subtract cursor-row:address:number/deref, 1:literal
    right:number <- get editor:address:editor-data/deref, right:offset
    cursor-column:address:number/deref <- subtract right:number, 1:literal  # leave room for wrap icon
  }
]

recipe move-cursor-in-editor [
  default-space:address:array:location <- new location:type, 30:literal
  editor:address:editor-data <- next-ingredient
  t:touch-event <- next-ingredient
  # always reset focus to start
  in-focus?:address:boolean <- get-address editor:address:editor-data/deref, in-focus?:offset
  in-focus?:address:boolean/deref <- copy 0:literal/true
  click-column:number <- get t:touch-event, column:offset
  left:number <- get editor:address:editor-data/deref, left:offset
  too-far-left?:boolean <- lesser-than click-column:number, left:number
  reply-if too-far-left?:boolean
  right:number <- get editor:address:editor-data/deref, right:offset
  too-far-right?:boolean <- greater-than click-column:number, right:number
  reply-if too-far-right?:boolean
#?   $print [focus now at ], editor:address:editor-data, [ 
#? ] #? 2
  # click on this window; gain focus
  in-focus?:address:boolean/deref <- copy 1:literal/true
  # update cursor
  cursor-row:address:number <- get-address editor:address:editor-data/deref, cursor-row:offset
  cursor-row:address:number/deref <- get t:touch-event, row:offset
  cursor-column:address:number <- get-address editor:address:editor-data/deref, cursor-column:offset
  cursor-column:address:number/deref <- get t:touch-event, column:offset
#?   $print [column is at: ], cursor-column:address:number, [ 
#? ] #? 1
]

recipe insert-at-cursor [
  default-space:address:array:location <- new location:type, 30:literal
  editor:address:editor-data <- next-ingredient
  c:character <- next-ingredient
#?   $print [insert ], c:character, [ 
#? ] #? 1
  before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/deref, before-cursor:offset
  d:address:duplex-list <- get editor:address:editor-data/deref, data:offset
  insert-duplex c:character, before-cursor:address:address:duplex-list/deref
  screen:address <- get editor:address:editor-data/deref, screen:offset
  cursor-row:address:number <- get-address editor:address:editor-data/deref, cursor-row:offset
  cursor-column:address:number <- get-address editor:address:editor-data/deref, cursor-column:offset
  # update cursor: if newline, move cursor to start of next line
  # todo: bottom of screen
  {
    newline?:boolean <- equal c:character, 10:literal/newline
    break-unless newline?:boolean
    cursor-row:address:number/deref <- add cursor-row:address:number/deref, 1:literal
    cursor-column:address:number/deref <- copy 0:literal
    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
    right:number <- get editor:address:editor-data/deref, right:offset
    wrap-column:number <- subtract right:number, 1:literal
#?     $print [wrap? ], cursor-column:address:number/deref, [ vs ], wrap-column:number, [ 
#? ] #? 1
    at-wrap?:boolean <- greater-or-equal cursor-column:address:number/deref, wrap-column:number
    break-unless at-wrap?:boolean
#?     $print [wrap!
#? ] #? 1
    cursor-column:address:number/deref <- subtract cursor-column:address:number/deref, wrap-column:number
    cursor-row:address:number/deref <- add cursor-row:address:number/deref, 1:literal
    # 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/deref, 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/deref <- add cursor-column:address:number/deref, 1:literal
]

recipe delete-before-cursor [
  default-space:address:array:location <- new location:type, 30:literal
  editor:address:editor-data <- next-ingredient
  before-cursor:address:address:duplex-list <- get-address editor:address:editor-data/deref, before-cursor:offset
  d:address:duplex-list <- get editor:address:editor-data/deref, data:offset
  # unless already at start
  at-start?:boolean <- equal before-cursor:address:address:duplex-list/deref, d:address:duplex-list
  reply-if at-start?:boolean
  # delete character
  prev:address:duplex-list <- prev-duplex before-cursor:address:address:duplex-list/deref
  remove-duplex before-cursor:address:address:duplex-list/deref
  # update cursor
  before-cursor:address:address:duplex-list/deref <- copy prev:address:duplex-list
  cursor-column:address:number <- get-address editor:address:editor-data/deref, cursor-column:offset
  cursor-column:address:number/deref <- subtract cursor-column:address:number/deref, 1:literal
]

# 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 [
  default-space:address:array:location <- new location:type, 30:literal
  curr:address:duplex-list <- next-ingredient
  start:address:duplex-list <- next-ingredient
  result:number <- copy 0:literal
  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/deref, value:offset
    at-newline?:boolean <- equal c:character 10:literal/newline
    break-if at-newline?:boolean
    result:number <- add result:number, 1:literal
    loop
  }
  reply result:number
]

scenario editor-handles-empty-event-queue [
  assume-screen 10:literal/width, 5:literal/height
#?   3:number <- get screen:address/deref, num-rows:offset #? 1
#?   $print [0: ], screen:address, [: ], 3:number, [ 
#? ] #? 1
  1:address:array:character <- new [abc]
#?   $print [1: ], screen:address, [ 
#? ] #? 1
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console []
#?   $print [8: ], screen:address, [ 
#? ] #? 1
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
#?   $print [9: ], screen:address, [ 
#? ] #? 1
  screen-should-contain [
    .abc       .
    .          .
  ]
]

scenario editor-handles-mouse-clicks [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 0, 1  # on the 'b'
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  screen-should-contain [
    .abc       .
    .          .
  ]
  memory-should-contain [
    3 <- 0  # cursor is at row 0..
    4 <- 1  # ..and column 1
  ]
]

scenario editor-handles-mouse-clicks-outside-text [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 0, 7  # last line, to the right of text
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 0  # cursor row
    4 <- 3  # cursor column
  ]
]

scenario editor-handles-mouse-clicks-outside-text-2 [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc
def]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 0, 7  # interior line, to the right of text
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 0  # cursor row
    4 <- 3  # cursor column
  ]
]

scenario editor-handles-mouse-clicks-outside-text-3 [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc
def]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 2, 7  # below text
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 1  # cursor row
    4 <- 3  # cursor column
  ]
]

scenario editor-handles-mouse-clicks-outside-column [
  assume-screen 10:literal/width, 5:literal/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:literal/top, 0:literal/left, 5:literal/right
  assume-console [
    # click on right half of screen
    left-click 3, 8
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  screen-should-contain [
    .abc       .
    .          .
  ]
  memory-should-contain [
    3 <- 0  # no change to cursor row
    4 <- 0  # ..or column
  ]
]

scenario editor-inserts-characters-into-empty-editor [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new []
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  assume-console [
    type [abc]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .abc       .
    .          .
  ]
]

scenario editor-inserts-characters-at-cursor [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    type [0]
    left-click 0, 2
    type [d]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .0adbc     .
    .          .
  ]
]

scenario editor-inserts-characters-at-cursor-2 [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 0, 5  # right of last line
    type [d]  # should append
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .abcd      .
    .          .
  ]
]

scenario editor-inserts-characters-at-cursor-3 [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 3, 5  # below all text
    type [d]  # should append
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .abcd      .
    .          .
  ]
]

scenario editor-inserts-characters-at-cursor-4 [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc
d]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 3, 5  # below all text
    type [e]  # should append
  ]
  run [
    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:literal/width, 5:literal/height
  1:address:array:character <- new [abc
d]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 3, 5  # below all text
    type [ef]  # should append multiple characters in order
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .abc       .
    .def       .
    .          .
  ]
]

scenario editor-wraps-line-on-insert [
  assume-screen 5:literal/width, 3:literal/height
  1:address:array:character <- new [abcd]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  # type a letter
  assume-console [
    type [e]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  # no wrap yet
  screen-should-contain [
    .eabcd.
    .     .
  ]
  # type a second letter
  assume-console [
    type [f]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  # now wrap
  screen-should-contain [
    .efab.
    .cd   .
    .     .
  ]
]

scenario editor-moves-cursor-after-inserting-characters [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  assume-console [
    type [01]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .01abc     .
    .          .
  ]
]

scenario editor-wraps-cursor-after-inserting-characters [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abcde]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  assume-console [
    left-click 0, 4  # line is full; no wrap icon yet
    type [f]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  screen-should-contain [
    .abcd     .
    .fe        .
    .          .
  ]
  memory-should-contain [
    3 <- 1  # cursor row
    4 <- 1  # cursor column
  ]
]

scenario editor-wraps-cursor-after-inserting-characters-2 [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abcde]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  assume-console [
    left-click 0, 3  # right before the wrap icon
    type [f]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  screen-should-contain [
    .abcf     .
    .de        .
    .          .
  ]
  memory-should-contain [
    3 <- 1  # cursor row
    4 <- 0  # cursor column
  ]
]

scenario editor-moves-cursor-down-after-inserting-newline [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    type [0
1]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .0         .
    .1abc      .
    .          .
  ]
]

scenario editor-handles-backspace-key [
#?   $print [=== new test
#? ] #? 1
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
#?   $print [editor: ], 2:address:editor-data, [ 
#? ] #? 1
  assume-console [
    left-click 0, 1
    type [«]
  ]
  3:event/backspace <- merge 0:literal/text, 8:literal/backspace, 0:literal/dummy, 0:literal/dummy
  replace-in-console 171:literal/«, 3:event/backspace
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    4:number <- get 2:address:editor-data/deref, cursor-row:offset
    5:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  screen-should-contain [
    .bc        .
    .          .
  ]
  memory-should-contain [
    4 <- 0
    5 <- 0
  ]
]

scenario editor-moves-cursor-right-with-key [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    press 65514  # right arrow
    type [0]
  ]
  run [
    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:literal/width, 5:literal/height
  1:address:array:character <- new [abc
d]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    press 65514  # right arrow
    press 65514  # right arrow
    press 65514  # right arrow
    press 65514  # right arrow - next line
    type [0]
  ]
  run [
    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:literal/width, 5:literal/height
  1:address:array:character <- new [abcdef]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  assume-console [
    left-click 0, 3
    press 65514  # right arrow
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  screen-should-contain [
    .abcd     .
    .ef        .
    .          .
  ]
  memory-should-contain [
    3 <- 1
    4 <- 0
  ]
]

scenario editor-does-not-wrap-cursor-when-line-does-not-wrap [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abcde]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  assume-console [
    left-click 0, 3
    press 65514  # right arrow
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 0
    4 <- 4
  ]
]

scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [
  assume-screen 10:literal/width, 5:literal/height
  1:address:array:character <- new [abc
d]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 0, 3
    press 65514  # right arrow - next line
    type [0]
  ]
  run [
    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:literal/width, 5:literal/height
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  assume-console [
    left-click 0, 2
    press 65515  # left arrow
    type [0]
  ]
  run [
    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:literal/width, 5:literal/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:literal/top, 0:literal/left, 10:literal/right
  # position cursor at start of second line (so there's no previous newline)
  assume-console [
    left-click 1, 0
    press 65515  # left arrow
    type [0]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
  ]
  screen-should-contain [
    .abc0      .
    .d         .
    .          .
  ]
]

scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [
  assume-screen 10:literal/width, 5:literal/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:literal/top, 0:literal/left, 10:literal/right
  # position cursor further down (so there's a newline before the character at
  # the cursor)
  assume-console [
    left-click 2, 0
    press 65515  # left arrow
    type [0]
  ]
  run [
    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:literal/width, 5:literal/height
  1:address:array:character <- new [abc
def
g]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 10:literal/right
  # position cursor at start of text
  assume-console [
    left-click 0, 0
    press 65515  # left arrow should have no effect
    type [0]
  ]
  run [
    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:literal/width, 5:literal/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:literal/top, 0:literal/left, 10:literal/right
  # position cursor right after empty line
  assume-console [
    left-click 2, 0
    press 65515  # left arrow
    type [0]
  ]
  run [
    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:literal/width, 5:literal/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:literal/top, 0:literal/left, 5:literal/right
  screen-should-contain [
    .abcd     .
    .ef        .
    .          .
  ]
  # position cursor right after empty line
  assume-console [
    left-click 1, 0
    press 65515  # left arrow
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    3:number <- get 2:address:editor-data/deref, cursor-row:offset
    4:number <- get 2:address:editor-data/deref, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 0  # previous row
    4 <- 3  # end of wrapped line
  ]
]

scenario point-at-multiple-editors [
  assume-screen 10:literal/width, 5:literal/height
  # initialize an editor covering left half of screen
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  3:address:array:character <- new [def]
  # chain new editor to it, covering the right half of the screen
  4:address:address:editor-data <- get-address 2:address:editor-data/deref, next-editor:offset
  4:address:address:editor-data/deref <- new-editor 3:address:array:character, screen:address, 0:literal/top, 5:literal/left, 10:literal/right
  # type one letter in each of them
  assume-console [
    left-click 0, 1
    left-click 0, 8
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    5:number <- get 2:address:editor-data/deref, cursor-column:offset
    6:number <- get 4:address:address:editor-data/deref/deref, cursor-column:offset
  ]
  memory-should-contain [
    5 <- 1
    6 <- 8
  ]
]

scenario editors-chain-to-cover-multiple-columns [
  assume-screen 10:literal/width, 5:literal/height
  # initialize an editor covering left half of screen
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  3:address:array:character <- new [def]
  # chain new editor to it, covering the right half of the screen
  4:address:address:editor-data <- get-address 2:address:editor-data/deref, next-editor:offset
  4:address:address:editor-data/deref <- new-editor 3:address:array:character, screen:address, 0:literal/top, 5:literal/left, 10:literal/right
  reset-focus 2:address:editor-data
  # type one letter in each of them
  assume-console [
    left-click 0, 1
    type [0]
    left-click 0, 6
    type [1]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    5:number <- get 2:address:editor-data/deref, cursor-column:offset
    6:number <- get 4:address:address:editor-data/deref/deref, cursor-column:offset
  ]
  screen-should-contain [
    .a0bc d1ef .
    .          .
  ]
  memory-should-contain [
    5 <- 2
    6 <- 7
  ]
  # show the cursor at the right window
  run [
    screen:address <- print-character screen:address, 9251:literal/  ]
  screen-should-contain [
    .a0bc d1f .
    .          .
  ]
]

scenario editor-in-focus-keeps-cursor [
  assume-screen 10:literal/width, 5:literal/height
  # initialize an editor covering left half of screen
  1:address:array:character <- new [abc]
  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0:literal/top, 0:literal/left, 5:literal/right
  3:address:array:character <- new [def]
  # chain new editor to it, covering the right half of the screen
  4:address:address:editor-data <- get-address 2:address:editor-data/deref, next-editor:offset
  4:address:address:editor-data/deref <- new-editor 3:address:array:character, screen:address, 0:literal/top, 5:literal/left, 10:literal/right
  # initialize cursor
  run [
    reset-focus 2:address:editor-data
    5:number <- get 2:address:editor-data/deref, cursor-row:offset
    6:number <- get 2:address:editor-data/deref, cursor-column:offset
    move-cursor screen:address, 5:number, 6:number
    screen:address <- print-character screen:address, 9251:literal/  ]
  # is it at the right place?
  screen-should-contain [
    .bc  def  .
    .          .
  ]
  # now try typing a letter
  assume-console [
    type [z]
  ]
  run [
    event-loop screen:address, console:address, 2:address:editor-data
    screen:address <- print-character screen:address, 9251:literal/  ]
  # cursor should still be right
  screen-should-contain [
    .zbc def  .
    .          .
  ]
]

# set focus to first editor, reset it in later ones
recipe reset-focus [
  default-space:address:array:location <- new location:type, 30:literal
  editor:address:editor-data <- next-ingredient
  in-focus:address:boolean <- get-address editor:address:editor-data/deref, in-focus?:offset
  in-focus:address:boolean/deref <- copy 1:literal/true
  e:address:editor-data <- get editor:address:editor-data/deref, next-editor:offset
  {
    break-unless e:address:editor-data
#?     $print [resetting focus in ], e:address:editor-data, [ 
#? ] #? 1
    x:address:boolean <- get-address e:address:editor-data/deref, in-focus?:offset
#?     $print [ at ], x:address:boolean, [ 
#? ] #? 1
    x:address:boolean/deref <- copy 0:literal/false
    e:address:editor-data <- get e:address:editor-data/deref, next-editor:offset
    loop
  }
]

## helpers for drawing editor borders

recipe draw-box [
  default-space:address:array:location <- new location:type, 30:literal
  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:literal/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 [
  default-space:address:array:location <- new location:type, 30:literal
  screen:address <- next-ingredient
  row:number <- next-ingredient
  x: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:literal/grey
  }
  move-cursor screen:address, row:number, x:number
  {
    continue?:boolean <- lesser-than x:number, right:number
    break-unless continue?:boolean
    print-character screen:address, 9472:literal/horizontal, color:number
    x:number <- add x:number, 1:literal
    loop
  }
]

recipe draw-vertical [
  default-space:address:array:location <- new location:type, 30:literal
  screen:address <- next-ingredient
  col:number <- next-ingredient
  x:number <- next-ingredient
  bottom:number <- next-ingredient
  color:number, color-found?:boolean <- next-ingredient
  {
    # default color to white
    break-if color-found?:boolean
    color:number <- copy 245:literal/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, 9474:literal/vertical, color:number
    x:number <- add x:number, 1:literal
    loop
  }
]

recipe draw-top-left [
  default-space:address:array:location <- new location:type, 30:literal
  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:literal/grey
  }
  move-cursor screen:address, top:number, left:number
  print-character screen:address, 9484:literal/down-right, color:number
]

recipe draw-top-right [
  default-space:address:array:location <- new location:type, 30:literal
  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:literal/grey
  }
  move-cursor screen:address, top:number, right:number
  print-character screen:address, 9488:literal/down-left, color:number
]

recipe draw-bottom-left [
  default-space:address:array:location <- new location:type, 30:literal
  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:literal/grey
  }
  move-cursor screen:address, bottom:number, left:number
  print-character screen:address, 9492:literal/up-right, color:number
]

recipe draw-bottom-right [
  default-space:address:array:location <- new location:type, 30:literal
  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:literal/grey
  }
  move-cursor screen:address, bottom:number, right:number
  print-character screen:address, 9496:literal/up-left, color:number
]