# Mu - edit/004-programming-environment.mu

      1 ## putting the environment together out of editors
      2 #
      3 # Consists of one editor on the left for recipes and one on the right for the
      4 # sandbox.
      5
      6 def! main [
      7   local-scope
      8   open-console
      9   clear-screen null/screen  # non-scrolling app
     10   env:&:environment <- new-programming-environment null/filesystem, null/screen
     11   render-all null/screen, env, render
     12   event-loop null/screen, null/console, env, null/filesystem
     13 ]
     14
     15 container environment [
     16   recipes:&:editor
     17   current-sandbox:&:editor
     18   sandbox-in-focus?:bool  # false => cursor in recipes; true => cursor in current-sandbox
     19 ]
     20
     21 def new-programming-environment resources:&:resources, screen:&:screen, test-sandbox-editor-contents:text -> result:&:environment [
     22   local-scope
     23   load-inputs
     24   width:num <- screen-width screen
     25   result <- new environment:type
     26   # recipe editor on the left
     27   initial-recipe-contents:text <- slurp resources, [lesson/recipes.mu]  # ignore errors
     28   divider:num, _ <- divide-with-remainder width, 2
     29   recipes:&:editor <- new-editor initial-recipe-contents, 0/left, divider/right
     30   # sandbox editor on the right
     31   sandbox-left:num <- add divider, 1
     32   current-sandbox:&:editor <- new-editor test-sandbox-editor-contents, sandbox-left, width/right
     33   *result <- put *result, recipes:offset, recipes
     34   *result <- put *result, current-sandbox:offset, current-sandbox
     35   *result <- put *result, sandbox-in-focus?:offset, false
     36   <programming-environment-initialization>
     37 ]
     38
     39 def event-loop screen:&:screen, console:&:console, env:&:environment, resources:&:resources -> screen:&:screen, console:&:console, env:&:environment, resources:&:resources [
     40   local-scope
     41   load-inputs
     42   recipes:&:editor <- get *env, recipes:offset
     43   current-sandbox:&:editor <- get *env, current-sandbox:offset
     44   sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
     45   # if we fall behind we'll stop updating the screen, but then we have to
     46   # render the entire screen when we catch up.
     47   # todo: test this
     48   render-recipes-on-no-more-events?:bool <- copy false
     49   render-sandboxes-on-no-more-events?:bool <- copy false
     50   {
     51     # looping over each (keyboard or touch) event as it occurs
     52     +next-event
     53     e:event, found?:bool, quit?:bool, console <- read-event console
     54     loop-unless found?
     55     break-if quit?  # only in tests
     56     trace 10, [app], [next-event]
     57     <handle-event>
     58     # check for global events that will trigger regardless of which editor has focus
     59     {
     60       k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
     61       break-unless is-keycode?
     62       <global-keypress>
     63     }
     64     {
     65       c:char, is-unicode?:bool <- maybe-convert e:event, text:variant
     66       break-unless is-unicode?
     67       <global-type>
     68     }
     69     # 'touch' event - send to both sides, see what picks it up
     70     {
     71       t:touch-event, is-touch?:bool <- maybe-convert e:event, touch:variant
     72       break-unless is-touch?
     73       # ignore all but 'left-click' events for now
     74       # todo: test this
     75       touch-type:num <- get t, type:offset
     76       is-left-click?:bool <- equal touch-type, 65513/mouse-left
     77       loop-unless is-left-click?, +next-event
     78       click-row:num <- get t, row:offset
     79       click-column:num <- get t, column:offset
     80       # later exceptions for non-editor touches will go here
     81       <global-touch>
     82       # send to both editors
     83       _ <- move-cursor recipes, screen, t
     84       sandbox-in-focus?:bool <- move-cursor current-sandbox, screen, t
     85       *env <- put *env, sandbox-in-focus?:offset, sandbox-in-focus?
     86       screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
     87       loop +next-event
     88     }
     89     # 'resize' event - redraw editor
     90     # todo: test this after supporting resize in assume-console
     91     {
     92       r:resize-event, is-resize?:bool <- maybe-convert e:event, resize:variant
     93       break-unless is-resize?
     94       env, screen <- resize screen, env
     95       screen <- render-all screen, env, render-without-moving-cursor
     96       loop +next-event
     97     }
     98     # if it's not global and not a touch event, send to appropriate editor
     99     {
    100       sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
    101       {
    102         break-if sandbox-in-focus?
    103         render?:bool <- handle-keyboard-event screen, recipes, e:event
    104         render-recipes-on-no-more-events? <- or render?, render-recipes-on-no-more-events?
    105       }
    106       {
    107         break-unless sandbox-in-focus?
    108         render?:bool <- handle-keyboard-event screen, current-sandbox, e:event
    109         render-sandboxes-on-no-more-events? <- or render?, render-sandboxes-on-no-more-events?
    110       }
    111       more-events?:bool <- has-more-events? console
    112       {
    113         break-if more-events?
    114         {
    115           break-unless render-recipes-on-no-more-events?
    116           render-recipes-on-no-more-events? <- copy false
    117           screen <- render-recipes screen, env, render
    118         }
    119         {
    120           break-unless render-sandboxes-on-no-more-events?
    121           render-sandboxes-on-no-more-events? <- copy false
    122           screen <- render-sandbox-side screen, env, render
    123         }
    124       }
    125       screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
    126     }
    127     loop
    128   }
    129 ]
    130
    131 def resize screen:&:screen, env:&:environment -> env:&:environment, screen:&:screen [
    132   local-scope
    133   load-inputs
    134   clear-screen screen  # update screen dimensions
    135   width:num <- screen-width screen
    136   divider:num, _ <- divide-with-remainder width, 2
    137   # update recipe editor
    138   recipes:&:editor <- get *env, recipes:offset
    139   right:num <- subtract divider, 1
    140   *recipes <- put *recipes, right:offset, right
    141   # reset cursor (later we'll try to preserve its position)
    142   *recipes <- put *recipes, cursor-row:offset, 1
    143   *recipes <- put *recipes, cursor-column:offset, 0
    144   # update sandbox editor
    145   current-sandbox:&:editor <- get *env, current-sandbox:offset
    146   left:num <- add divider, 1
    147   *current-sandbox <- put *current-sandbox, left:offset, left
    148   right:num <- subtract width, 1
    149   *current-sandbox <- put *current-sandbox, right:offset, right
    150   # reset cursor (later we'll try to preserve its position)
    151   *current-sandbox <- put *current-sandbox, cursor-row:offset, 1
    152   *current-sandbox <- put *current-sandbox, cursor-column:offset, left
    153 ]
    154
    155 # Variant of 'render' that updates cursor-row and cursor-column based on
    156 # before-cursor (rather than the other way around). If before-cursor moves
    157 # off-screen, it resets cursor-row and cursor-column.
    158 def render-without-moving-cursor screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
    159   local-scope
    160   load-inputs
    161   return-unless editor, 1/top, 0/left
    162   left:num <- get *editor, left:offset
    163   screen-height:num <- screen-height screen
    164   right:num <- get *editor, right:offset
    165   curr:&:duplex-list:char <- get *editor, top-of-screen:offset
    166   prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
    167   curr <- next curr
    168   color:num <- copy 7/white
    169   row:num <- copy 1/top
    170   column:num <- copy left
    171   # save before-cursor
    172   old-before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
    173   # initialze cursor-row/cursor-column/before-cursor to the top of the screen
    174   # by default
    175   *editor <- put *editor, cursor-row:offset, row
    176   *editor <- put *editor, cursor-column:offset, column
    177   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
    178   *editor <- put *editor, before-cursor:offset, top-of-screen
    179   screen <- move-cursor screen, row, column
    180   {
    181     +next-character
    182     break-unless curr
    183     off-screen?:bool <- greater-or-equal row, screen-height
    184     break-if off-screen?
    185     # if we find old-before-cursor still on the new resized screen, update
    186     # editor.cursor-row and editor.cursor-column based on
    187     # old-before-cursor
    188     {
    189       at-cursor?:bool <- equal old-before-cursor, prev
    190       break-unless at-cursor?
    191       *editor <- put *editor, cursor-row:offset, row
    192       *editor <- put *editor, cursor-column:offset, column
    193       *editor <- put *editor, before-cursor:offset, old-before-cursor
    194     }
    195     c:char <- get *curr, value:offset
    196     <character-c-received>
    197     {
    198       # newline? move to left rather than 0
    199       newline?:bool <- equal c, 10/newline
    200       break-unless newline?
    201       # clear rest of line in this window
    202       clear-line-until screen, right
    203       # skip to next line
    204       row <- add row, 1
    205       column <- copy left
    206       screen <- move-cursor screen, row, column
    207       curr <- next curr
    208       prev <- next prev
    209       loop +next-character
    210     }
    211     {
    212       # at right? wrap. even if there's only one more letter left; we need
    213       # room for clicking on the cursor after it.
    214       at-right?:bool <- equal column, right
    215       break-unless at-right?
    216       # print wrap icon
    217       wrap-icon:char <- copy 8617/loop-back-to-left
    218       print screen, wrap-icon, 245/grey
    219       column <- copy left
    220       row <- add row, 1
    221       screen <- move-cursor screen, row, column
    222       # don't increment curr
    223       loop +next-character
    224     }
    225     print screen, c, color
    226     curr <- next curr
    227     prev <- next prev
    228     column <- add column, 1
    229     loop
    230   }
    231   # save first character off-screen
    232   *editor <- put *editor, bottom-of-screen:offset, curr
    233   *editor <- put *editor, bottom:offset, row
    234   return row, column
    235 ]
    236
    237 scenario point-at-multiple-editors [
    238   local-scope
    239   trace-until 100/app  # trace too long
    240   assume-screen 30/width, 5/height
    241   # initialize both halves of screen
    242   assume-resources [
    243     [lesson/recipes.mu] <- [
    244       |abc|
    245     ]
    246   ]
    247   env:&:environment <- new-programming-environment resources, screen, [def]  # contents of sandbox editor
    248   # focus on both sides
    249   assume-console [
    250     left-click 1, 1
    251     left-click 1, 17
    252   ]
    253   # check cursor column in each
    254   run [
    255     event-loop screen, console, env, resources
    256     recipes:&:editor <- get *env, recipes:offset
    257     5:num/raw <- get *recipes, cursor-column:offset
    258     sandbox:&:editor <- get *env, current-sandbox:offset
    259     7:num/raw <- get *sandbox, cursor-column:offset
    260   ]
    261   memory-should-contain [
    262     5 <- 1
    263     7 <- 17
    264   ]
    265 ]
    266
    267 scenario edit-multiple-editors [
    268   local-scope
    269   trace-until 100/app  # trace too long
    270   assume-screen 30/width, 5/height
    271   # initialize both halves of screen
    272   assume-resources [
    273     [lesson/recipes.mu] <- [
    274       |abc|
    275     ]
    276   ]
    277   env:&:environment <- new-programming-environment resources, screen, [def]  # contents of sandbox
    278   render-all screen, env, render
    279   # type one letter in each of them
    280   assume-console [
    281     left-click 1, 1
    282     type [0]
    283     left-click 1, 17
    284     type [1]
    285   ]
    286   run [
    287     event-loop screen, console, env, resources
    288     recipes:&:editor <- get *env, recipes:offset
    289     5:num/raw <- get *recipes, cursor-column:offset
    290     sandbox:&:editor <- get *env, current-sandbox:offset
    291     7:num/raw <- get *sandbox, cursor-column:offset
    292   ]
    293   screen-should-contain [
    294     .           run (F4)           .  # this line has a different background, but we don't test that yet
    295     .a0bc           ╎d1ef          .
    296     .               ╎──────────────.
    297     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
    298     .               ╎              .
    299   ]
    300   memory-should-contain [
    301     5 <- 2  # cursor column of recipe editor
    302     7 <- 18  # cursor column of sandbox editor
    303   ]
    304   # show the cursor at the right window
    305   run [
    306     cursor:char <- copy 9251/␣
    307     print screen, cursor
    308   ]
    309   screen-should-contain [
    310     .           run (F4)           .
    311     .a0bc           ╎d1␣f          .
    312     .               ╎──────────────.
    313     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
    314     .               ╎              .
    315   ]
    316 ]
    317
    318 scenario editor-in-focus-keeps-cursor [
    319   local-scope
    320   trace-until 100/app  # trace too long
    321   assume-screen 30/width, 5/height
    322   assume-resources [
    323     [lesson/recipes.mu] <- [
    324       |abc|
    325     ]
    326   ]
    327   env:&:environment <- new-programming-environment resources, screen, [def]
    328   render-all screen, env, render
    329   # initialize programming environment and highlight cursor
    330   assume-console []
    331   run [
    332     event-loop screen, console, env, resources
    333     cursor:char <- copy 9251/␣
    334     print screen, cursor
    335   ]
    336   # is cursor at the right place?
    337   screen-should-contain [
    338     .           run (F4)           .
    339     .␣bc            ╎def           .
    340     .               ╎──────────────.
    341     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
    342     .               ╎              .
    343   ]
    344   # now try typing a letter
    345   assume-console [
    346     type [z]
    347   ]
    348   run [
    349     event-loop screen, console, env, resources
    350     cursor:char <- copy 9251/␣
    351     print screen, cursor
    352   ]
    353   # cursor should still be right
    354   screen-should-contain [
    355     .           run (F4)           .
    356     .z␣bc           ╎def           .
    357     .               ╎──────────────.
    358     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
    359     .               ╎              .
    360   ]
    361 ]
    362
    363 scenario backspace-in-sandbox-editor-joins-lines [
    364   local-scope
    365   trace-until 100/app  # trace too long
    366   assume-screen 30/width, 5/height
    367   assume-resources [
    368   ]
    369   # initialize sandbox side with two lines
    370   test-sandbox-editor-contents:text <- new [abc
    371 def]
    372   env:&:environment <- new-programming-environment resources, screen, test-sandbox-editor-contents
    373   render-all screen, env, render
    374   screen-should-contain [
    375     .           run (F4)           .
    376     .               ╎abc           .
    377     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎def           .
    378     .               ╎──────────────.
    379     .               ╎              .
    380   ]
    381   # position cursor at start of second line and hit backspace
    382   assume-console [
    383     left-click 2, 16
    384     press backspace
    385   ]
    386   run [
    387     event-loop screen, console, env, resources
    388     cursor:char <- copy 9251/␣
    389     print screen, cursor
    390   ]
    391   # cursor moves to end of old line
    392   screen-should-contain [
    393     .           run (F4)           .
    394     .               ╎abc␣ef        .
    395     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎──────────────.
    396     .               ╎              .
    397   ]
    398 ]
    399
    400 type render-recipe = (recipe (address screen) (address editor) -> number number (address screen) (address editor))
    401
    402 def render-all screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
    403   local-scope
    404   load-inputs
    405   trace 10, [app], [render all]
    406   # top menu
    407   trace 11, [app], [render top menu]
    408   width:num <- screen-width screen
    409   draw-horizontal screen, 0, 0/left, width, 32/space, 0/black, 238/grey
    410   button-start:num <- subtract width, 20
    411   button-on-screen?:bool <- greater-or-equal button-start, 0
    412   assert button-on-screen?, [screen too narrow for menu]
    413   screen <- move-cursor screen, 0/row, button-start
    414   print screen, [ run (F4) ], 255/white, 161/reddish
    415   # dotted line down the middle
    416   trace 11, [app], [render divider]
    417   divider:num, _ <- divide-with-remainder width, 2
    418   height:num <- screen-height screen
    419   draw-vertical screen, divider, 1/top, height, 9482/vertical-dotted
    420   #
    421   screen <- render-recipes screen, env, render-editor
    422   screen <- render-sandbox-side screen, env, render-editor
    423   <end-render-components>  # no early returns permitted
    424   #
    425   recipes:&:editor <- get *env, recipes:offset
    426   current-sandbox:&:editor <- get *env, current-sandbox:offset
    427   sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
    428   screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
    429 ]
    430
    431 def render-recipes screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
    432   local-scope
    433   load-inputs
    434   trace 11, [app], [render recipes]
    435   old-top-idx:num <- save-top-idx screen
    436   recipes:&:editor <- get *env, recipes:offset
    437   # render recipes
    438   left:num <- get *recipes, left:offset
    439   right:num <- get *recipes, right:offset
    440   row:num, column:num, screen <- call render-editor, screen, recipes
    441   <end-render-recipe-components>
    442   # draw dotted line after recipes
    443   draw-horizontal screen, row, left, right, 9480/horizontal-dotted
    444   row <- add row, 1
    445   clear-screen-from screen, row, left, left, right
    446   #
    447   assert-no-scroll screen, old-top-idx
    448 ]
    449
    450 # replaced in a later layer
    451 def render-sandbox-side screen:&:screen, env:&:environment, render-editor:render-recipe -> screen:&:screen, env:&:environment [
    452   local-scope
    453   load-inputs
    454   trace 11, [app], [render sandboxes]
    455   old-top-idx:num <- save-top-idx screen
    456   current-sandbox:&:editor <- get *env, current-sandbox:offset
    457   left:num <- get *current-sandbox, left:offset
    458   right:num <- get *current-sandbox, right:offset
    459   row:num, column:num, screen, current-sandbox <- call render-editor, screen, current-sandbox
    460   # draw solid line after code (you'll see why in later layers)
    461   draw-horizontal screen, row, left, right
    462   row <- add row, 1
    463   clear-screen-from screen, row, left, left, right
    464   #
    465   assert-no-scroll screen, old-top-idx
    466 ]
    467
    468 def update-cursor screen:&:screen, recipes:&:editor, current-sandbox:&:editor, sandbox-in-focus?:bool, env:&:environment -> screen:&:screen [
    469   local-scope
    470   load-inputs
    471   <update-cursor-special-cases>
    472   {
    473     break-if sandbox-in-focus?
    474     cursor-row:num <- get *recipes, cursor-row:offset
    475     cursor-column:num <- get *recipes, cursor-column:offset
    476   }
    477   {
    478     break-unless sandbox-in-focus?
    479     cursor-row:num <- get *current-sandbox, cursor-row:offset
    480     cursor-column:num <- get *current-sandbox, cursor-column:offset
    481   }
    482   screen <- move-cursor screen, cursor-row, cursor-column
    483 ]
    484
    485 # ctrl-n - switch focus
    486 # todo: test this
    487
    488 after <global-type> [
    489   {
    490     switch-side?:bool <- equal c, 14/ctrl-n
    491     break-unless switch-side?
    492     sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
    493     sandbox-in-focus? <- not sandbox-in-focus?
    494     *env <- put *env, sandbox-in-focus?:offset, sandbox-in-focus?
    495     screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
    496     loop +next-event
    497   }
    498 ]
    499
    500 ## helpers
    501
    502 def draw-vertical screen:&:screen, col:num, y:num, bottom:num -> screen:&:screen [
    503   local-scope
    504   load-inputs
    505   style:char, style-found?:bool <- next-input
    506   {
    507     break-if style-found?
    508     style <- copy 9474/vertical
    509   }
    510   color:num, color-found?:bool <- next-input
    511   {
    512     # default color to white
    513     break-if color-found?
    514     color <- copy 245/grey
    515   }
    516   {
    517     continue?:bool <- lesser-than y, bottom
    518     break-unless continue?
    519     screen <- move-cursor screen, y, col
    520     print screen, style, color
    521     y <- add y, 1
    522     loop
    523   }
    524 ]
    525
    526 scenario backspace-over-text [
    527   local-scope
    528   trace-until 100/app  # trace too long
    529   assume-screen 50/width, 15/height
    530   # recipes.mu is empty
    531   assume-resources [
    532   ]
    533   # sandbox editor contains an instruction without storing outputs
    534   env:&:environment <- new-programming-environment resources, screen, []
    535   # run the code in the editors
    536   assume-console [
    537     type [a]
    538     press backspace
    539   ]
    540   run [
    541     event-loop screen, console, env, resources
    542     10:num/raw <- get *screen, cursor-row:offset
    543     11:num/raw <- get *screen, cursor-column:offset
    544   ]
    545   memory-should-contain [
    546     10 <- 1
    547     11 <- 0
    548   ]
    549 ]