https://github.com/akkartik/mu/blob/master/edit/001-editor.mu

      1 ## the basic editor data structure, and how it displays text to the screen
      2
      3 # temporary main for this layer: just render the given text at the given
      4 # screen dimensions, then stop
      5 def main text:text [
      6   local-scope
      7   load-inputs
      8   open-console
      9   clear-screen null/screen  # non-scrolling app
     10   e:&:editor <- new-editor text, 0/left, 5/right
     11   render null/screen, e
     12   wait-for-event null/console
     13   close-console
     14 ]
     15
     16 scenario editor-renders-text-to-screen [
     17   local-scope
     18   assume-screen 10/width, 5/height
     19   e:&:editor <- new-editor [abc], 0/left, 10/right
     20   run [
     21     render screen, e
     22   ]
     23   screen-should-contain [
     24     # top line of screen reserved for menu
     25     .          .
     26     .abc       .
     27     .          .
     28   ]
     29 ]
     30
     31 container editor [
     32   # editable text: doubly linked list of characters (head contains a special sentinel)
     33   data:&:duplex-list:char
     34   top-of-screen:&:duplex-list:char
     35   bottom-of-screen:&:duplex-list:char
     36   # location before cursor inside data
     37   before-cursor:&:duplex-list:char
     38
     39   # raw bounds of display area on screen
     40   # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
     41   left:num
     42   right:num
     43   bottom:num
     44   # raw screen coordinates of cursor
     45   cursor-row:num
     46   cursor-column:num
     47 ]
     48
     49 # creates a new editor widget
     50 #   right is exclusive
     51 def new-editor s:text, left:num, right:num -> result:&:editor [
     52   local-scope
     53   load-inputs
     54   # no clipping of bounds
     55   right <- subtract right, 1
     56   result <- new editor:type
     57   # initialize screen-related fields
     58   *result <- put *result, left:offset, left
     59   *result <- put *result, right:offset, right
     60   # initialize cursor coordinates
     61   *result <- put *result, cursor-row:offset, 1/top
     62   *result <- put *result, cursor-column:offset, left
     63   # initialize empty contents
     64   init:&:duplex-list:char <- push 167/§, null
     65   *result <- put *result, data:offset, init
     66   *result <- put *result, top-of-screen:offset, init
     67   *result <- put *result, before-cursor:offset, init
     68   result <- insert-text result, s
     69   <editor-initialization>
     70 ]
     71
     72 def insert-text editor:&:editor, text:text -> editor:&:editor [
     73   local-scope
     74   load-inputs
     75   curr:&:duplex-list:char <- get *editor, data:offset
     76   insert curr, text
     77 ]
     78
     79 scenario editor-initializes-without-data [
     80   local-scope
     81   assume-screen 5/width, 3/height
     82   run [
     83     e:&:editor <- new-editor null/data, 2/left, 5/right
     84     1:editor/raw <- copy *e
     85   ]
     86   memory-should-contain [
     87     # 1,2 (data) <- just the § sentinel
     88     # 3,4 (top of screen) <- the § sentinel
     89     # 5 (bottom of screen) <- null since text fits on screen
     90     5 <- 0
     91     6 <- 0
     92     # 7,8 (before cursor) <- the § sentinel
     93     9 <- 2  # left
     94     10 <- 4  # right  (inclusive)
     95     11 <- 0  # bottom (not set until render)
     96     12 <- 1  # cursor row
     97     13 <- 2  # cursor column
     98   ]
     99   screen-should-contain [
    100     .     .
    101     .     .
    102     .     .
    103   ]
    104 ]
    105
    106 # Assumes cursor should be at coordinates (cursor-row, cursor-column) and
    107 # updates before-cursor to match. Might also move coordinates if they're
    108 # outside text.
    109 def render screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
    110   local-scope
    111   load-inputs
    112   return-unless editor, 1/top, 0/left
    113   left:num <- get *editor, left:offset
    114   screen-height:num <- screen-height screen
    115   right:num <- get *editor, right:offset
    116   # traversing editor
    117   curr:&:duplex-list:char <- get *editor, top-of-screen:offset
    118   prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
    119   curr <- next curr
    120   # traversing screen
    121   color:num <- copy 7/white
    122   row:num <- copy 1/top
    123   column:num <- copy left
    124   cursor-row:num <- get *editor, cursor-row:offset
    125   cursor-column:num <- get *editor, cursor-column:offset
    126   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
    127   screen <- move-cursor screen, row, column
    128   {
    129     +next-character
    130     break-unless curr
    131     off-screen?:bool <- greater-or-equal row, screen-height
    132     break-if off-screen?
    133     # update editor.before-cursor
    134     # Doing so at the start of each iteration ensures it stays one step behind
    135     # the current character.
    136     {
    137       at-cursor-row?:bool <- equal row, cursor-row
    138       break-unless at-cursor-row?
    139       at-cursor?:bool <- equal column, cursor-column
    140       break-unless at-cursor?
    141       before-cursor <- copy prev
    142     }
    143     c:char <- get *curr, value:offset
    144     <character-c-received>
    145     {
    146       # newline? move to left rather than 0
    147       newline?:bool <- equal c, 10/newline
    148       break-unless newline?
    149       # adjust cursor if necessary
    150       {
    151         at-cursor-row?:bool <- equal row, cursor-row
    152         break-unless at-cursor-row?
    153         left-of-cursor?:bool <- lesser-than column, cursor-column
    154         break-unless left-of-cursor?
    155         cursor-column <- copy column
    156         before-cursor <- prev curr
    157       }
    158       # clear rest of line in this window
    159       clear-line-until screen, right
    160       # skip to next line
    161       row <- add row, 1
    162       column <- copy left
    163       screen <- move-cursor screen, row, column
    164       curr <- next curr
    165       prev <- next prev
    166       loop +next-character
    167     }
    168     {
    169       # at right? wrap. even if there's only one more letter left; we need
    170       # room for clicking on the cursor after it.
    171       at-right?:bool <- equal column, right
    172       break-unless at-right?
    173       # print wrap icon
    174       wrap-icon:char <- copy 8617/loop-back-to-left
    175       print screen, wrap-icon, 245/grey
    176       column <- copy left
    177       row <- add row, 1
    178       screen <- move-cursor screen, row, column
    179       # don't increment curr
    180       loop +next-character
    181     }
    182     print screen, c, color
    183     curr <- next curr
    184     prev <- next prev
    185     column <- add column, 1
    186     loop
    187   }
    188   # save first character off-screen
    189   *editor <- put *editor, bottom-of-screen:offset, curr
    190   # is cursor to the right of the last line? move to end
    191   {
    192     at-cursor-row?:bool <- equal row, cursor-row
    193     cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
    194     before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
    195     above-cursor-row?:bool <- lesser-than row, cursor-row
    196     before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
    197     break-unless before-cursor?
    198     cursor-row <- copy row
    199     cursor-column <- copy column
    200     before-cursor <- copy prev
    201   }
    202   *editor <- put *editor, bottom:offset, row
    203   *editor <- put *editor, cursor-row:offset, cursor-row
    204   *editor <- put *editor, cursor-column:offset, cursor-column
    205   *editor <- put *editor, before-cursor:offset, before-cursor
    206   clear-line-until screen, right
    207   row <- add row, 1
    208   return row, left/column
    209 ]
    210
    211 def clear-screen-from screen:&:screen, row:num, column:num, left:num, right:num -> screen:&:screen [
    212   local-scope
    213   load-inputs
    214   # if it's the real screen, use the optimized primitive
    215   {
    216     break-if screen
    217     clear-display-from row, column, left, right
    218     return
    219   }
    220   # if not, go the slower route
    221   screen <- move-cursor screen, row, column
    222   clear-line-until screen, right
    223   clear-rest-of-screen screen, row, left, right
    224 ]
    225
    226 def clear-rest-of-screen screen:&:screen, row:num, left:num, right:num -> screen:&:screen [
    227   local-scope
    228   load-inputs
    229   row <- add row, 1
    230   # if it's the real screen, use the optimized primitive
    231   {
    232     break-if screen
    233     clear-display-from row, left, left, right
    234     return
    235   }
    236   screen <- move-cursor screen, row, left
    237   screen-height:num <- screen-height screen
    238   {
    239     at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
    240     break-if at-bottom-of-screen?
    241     screen <- move-cursor screen, row, left
    242     clear-line-until screen, right
    243     row <- add row, 1
    244     loop
    245   }
    246 ]
    247
    248 scenario editor-prints-multiple-lines [
    249   local-scope
    250   assume-screen 5/width, 5/height
    251   s:text <- new [abc
    252 def]
    253   e:&:editor <- new-editor s, 0/left, 5/right
    254   run [
    255     render screen, e
    256   ]
    257   screen-should-contain [
    258     .     .
    259     .abc  .
    260     .def  .
    261     .     .
    262   ]
    263 ]
    264
    265 scenario editor-handles-offsets [
    266   local-scope
    267   assume-screen 5/width, 5/height
    268   e:&:editor <- new-editor [abc], 1/left, 5/right
    269   run [
    270     render screen, e
    271   ]
    272   screen-should-contain [
    273     .     .
    274     . abc .
    275     .     .
    276   ]
    277 ]
    278
    279 scenario editor-prints-multiple-lines-at-offset [
    280   local-scope
    281   assume-screen 5/width, 5/height
    282   s:text <- new [abc
    283 def]
    284   e:&:editor <- new-editor s, 1/left, 5/right
    285   run [
    286     render screen, e
    287   ]
    288   screen-should-contain [
    289     .     .
    290     . abc .
    291     . def .
    292     .     .
    293   ]
    294 ]
    295
    296 scenario editor-wraps-long-lines [
    297   local-scope
    298   assume-screen 5/width, 5/height
    299   e:&:editor <- new-editor [abc def], 0/left, 5/right
    300   run [
    301     render screen, e
    302   ]
    303   screen-should-contain [
    304     .     .
    305     .abc ↩.
    306     .def  .
    307     .     .
    308   ]
    309   screen-should-contain-in-color 245/grey [
    310     .     .
    311     .    ↩.
    312     .     .
    313     .     .
    314   ]
    315 ]
    316
    317 scenario editor-wraps-barely-long-lines [
    318   local-scope
    319   assume-screen 5/width, 5/height
    320   e:&:editor <- new-editor [abcde], 0/left, 5/right
    321   run [
    322     render screen, e
    323   ]
    324   # still wrap, even though the line would fit. We need room to click on the
    325   # end of the line
    326   screen-should-contain [
    327     .     .
    328     .abcd↩.
    329     .e    .
    330     .     .
    331   ]
    332   screen-should-contain-in-color 245/grey [
    333     .     .
    334     .    ↩.
    335     .     .
    336     .     .
    337   ]
    338 ]
    339
    340 scenario editor-with-empty-text [
    341   local-scope
    342   assume-screen 5/width, 5/height
    343   e:&:editor <- new-editor [], 0/left, 5/right
    344   run [
    345     render screen, e
    346     3:num/raw <- get *e, cursor-row:offset
    347     4:num/raw <- get *e, cursor-column:offset
    348   ]
    349   screen-should-contain [
    350     .     .
    351     .     .
    352     .     .
    353   ]
    354   memory-should-contain [
    355     3 <- 1  # cursor row
    356     4 <- 0  # cursor column
    357   ]
    358 ]
    359
    360 # just a little color for Mu code
    361
    362 scenario render-colors-comments [
    363   local-scope
    364   assume-screen 5/width, 5/height
    365   s:text <- new [abc
    366 # de
    367 f]
    368   e:&:editor <- new-editor s, 0/left, 5/right
    369   run [
    370     render screen, e
    371   ]
    372   screen-should-contain [
    373     .     .
    374     .abc  .
    375     .# de .
    376     .f    .
    377     .     .
    378   ]
    379   screen-should-contain-in-color 12/lightblue, [
    380     .     .
    381     .     .
    382     .# de .
    383     .     .
    384     .     .
    385   ]
    386   screen-should-contain-in-color 7/white, [
    387     .     .
    388     .abc  .
    389     .     .
    390     .f    .
    391     .     .
    392   ]
    393 ]
    394
    395 after <character-c-received> [
    396   color <- get-color color, c
    397 ]
    398
    399 # so far the previous color is all the information we need; that may change
    400 def get-color color:num, c:char -> color:num [
    401   local-scope
    402   load-inputs
    403   color-is-white?:bool <- equal color, 7/white
    404   # if color is white and next character is '#', switch color to blue
    405   {
    406     break-unless color-is-white?
    407     starting-comment?:bool <- equal c, 35/#
    408     break-unless starting-comment?
    409     trace 90, [app], [switch color back to blue]
    410     return 12/lightblue
    411   }
    412   # if color is blue and next character is newline, switch color to white
    413   {
    414     color-is-blue?:bool <- equal color, 12/lightblue
    415     break-unless color-is-blue?
    416     ending-comment?:bool <- equal c, 10/newline
    417     break-unless ending-comment?
    418     trace 90, [app], [switch color back to white]
    419     return 7/white
    420   }
    421   # if color is white (no comments) and next character is '<', switch color to red
    422   {
    423     break-unless color-is-white?
    424     starting-assignment?:bool <- equal c, 60/<
    425     break-unless starting-assignment?
    426     return 1/red
    427   }
    428   # if color is red and next character is space, switch color to white
    429   {
    430     color-is-red?:bool <- equal color, 1/red
    431     break-unless color-is-red?
    432     ending-assignment?:bool <- equal c, 32/space
    433     break-unless ending-assignment?
    434     return 7/white
    435   }
    436   # otherwise no change
    437   return color
    438 ]
    439
    440 scenario render-colors-assignment [
    441   local-scope
    442   assume-screen 8/width, 5/height
    443   s:text <- new [abc
    444 d <- e
    445 f]
    446   e:&:editor <- new-editor s, 0/left, 8/right
    447   run [
    448     render screen, e
    449   ]
    450   screen-should-contain [
    451     .        .
    452     .abc     .
    453     .d <- e  .
    454     .f       .
    455     .        .
    456   ]
    457   screen-should-contain-in-color 1/red, [
    458     .        .
    459     .        .
    460     .  <-    .
    461     .        .
    462     .        .
    463   ]
    464 ]