https://github.com/akkartik/mu/blob/master/edit/002-typing.mu

       1 ## handling events from the keyboard, mouse, touch screen, ...
       2
       3 # temporary main: interactive editor
       4 # hit ctrl-c to exit
       5 def! main text:text [
       6   local-scope
       7   load-inputs
       8   open-console
       9   clear-screen null/screen  # non-scrolling app
      10   editor:&:editor <- new-editor text, 5/left, 45/right
      11   editor-render null/screen, editor
      12   editor-event-loop null/screen, null/console, editor
      13   close-console
      14 ]
      15
      16 def editor-event-loop screen:&:screen, console:&:console, editor:&:editor -> screen:&:screen, console:&:console, editor:&:editor [
      17   local-scope
      18   load-inputs
      19   {
      20     # looping over each (keyboard or touch) event as it occurs
      21     +next-event
      22     cursor-row:num <- get *editor, cursor-row:offset
      23     cursor-column:num <- get *editor, cursor-column:offset
      24     screen <- move-cursor screen, cursor-row, cursor-column
      25     e:event, found?:bool, quit?:bool, console <- read-event console
      26     loop-unless found?
      27     break-if quit?  # only in tests
      28     trace 10, [app], [next-event]
      29     # 'touch' event
      30     t:touch-event, is-touch?:bool <- maybe-convert e, touch:variant
      31     {
      32       break-unless is-touch?
      33       move-cursor editor, screen, t
      34       loop +next-event
      35     }
      36     # keyboard events
      37     {
      38       break-if is-touch?
      39       go-render?:bool <- handle-keyboard-event screen, editor, e
      40       {
      41         break-unless go-render?
      42         screen <- editor-render screen, editor
      43       }
      44     }
      45     loop
      46   }
      47 ]
      48
      49 # process click, return if it was on current editor
      50 def move-cursor editor:&:editor, screen:&:screen, t:touch-event -> in-focus?:bool, editor:&:editor [
      51   local-scope
      52   load-inputs
      53   return-unless editor, false
      54   click-row:num <- get t, row:offset
      55   return-unless click-row, false  # ignore clicks on 'menu'
      56   click-column:num <- get t, column:offset
      57   left:num <- get *editor, left:offset
      58   too-far-left?:bool <- lesser-than click-column, left
      59   return-if too-far-left?, false
      60   right:num <- get *editor, right:offset
      61   too-far-right?:bool <- greater-than click-column, right
      62   return-if too-far-right?, false
      63   # position cursor
      64   <begin-move-cursor>
      65   editor <- snap-cursor editor, screen, click-row, click-column
      66   undo-coalesce-tag:num <- copy 0/never
      67   <end-move-cursor>
      68   # gain focus
      69   return true
      70 ]
      71
      72 # Variant of 'render' that only moves the cursor (coordinates and
      73 # before-cursor). If it's past the end of a line, it 'slides' it left. If it's
      74 # past the last line it positions at end of last line.
      75 def snap-cursor editor:&:editor, screen:&:screen, target-row:num, target-column:num -> editor:&:editor [
      76   local-scope
      77   load-inputs
      78   return-unless editor
      79   left:num <- get *editor, left:offset
      80   right:num <- get *editor, right:offset
      81   screen-height:num <- screen-height screen
      82   # count newlines until screen row
      83   curr:&:duplex-list:char <- get *editor, top-of-screen:offset
      84   prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
      85   curr <- next curr
      86   row:num <- copy 1/top
      87   column:num <- copy left
      88   *editor <- put *editor, cursor-row:offset, target-row
      89   cursor-row:num <- copy target-row
      90   *editor <- put *editor, cursor-column:offset, target-column
      91   cursor-column:num <- copy target-column
      92   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
      93   {
      94     +next-character
      95     break-unless curr
      96     off-screen?:bool <- greater-or-equal row, screen-height
      97     break-if off-screen?
      98     # update editor.before-cursor
      99     # Doing so at the start of each iteration ensures it stays one step behind
     100     # the current character.
     101     {
     102       at-cursor-row?:bool <- equal row, cursor-row
     103       break-unless at-cursor-row?
     104       at-cursor?:bool <- equal column, cursor-column
     105       break-unless at-cursor?
     106       before-cursor <- copy prev
     107       *editor <- put *editor, before-cursor:offset, before-cursor
     108     }
     109     c:char <- get *curr, value:offset
     110     {
     111       # newline? move to left rather than 0
     112       newline?:bool <- equal c, 10/newline
     113       break-unless newline?
     114       # adjust cursor if necessary
     115       {
     116         at-cursor-row?:bool <- equal row, cursor-row
     117         break-unless at-cursor-row?
     118         left-of-cursor?:bool <- lesser-than column, cursor-column
     119         break-unless left-of-cursor?
     120         cursor-column <- copy column
     121         *editor <- put *editor, cursor-column:offset, cursor-column
     122         before-cursor <- copy prev
     123         *editor <- put *editor, before-cursor:offset, before-cursor
     124       }
     125       # skip to next line
     126       row <- add row, 1
     127       column <- copy left
     128       curr <- next curr
     129       prev <- next prev
     130       loop +next-character
     131     }
     132     {
     133       # at right? wrap. even if there's only one more letter left; we need
     134       # room for clicking on the cursor after it.
     135       at-right?:bool <- equal column, right
     136       break-unless at-right?
     137       column <- copy left
     138       row <- add row, 1
     139       # don't increment curr/prev
     140       loop +next-character
     141     }
     142     curr <- next curr
     143     prev <- next prev
     144     column <- add column, 1
     145     loop
     146   }
     147   # is cursor to the right of the last line? move to end
     148   {
     149     at-cursor-row?:bool <- equal row, cursor-row
     150     cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
     151     before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
     152     above-cursor-row?:bool <- lesser-than row, cursor-row
     153     before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
     154     break-unless before-cursor?
     155     cursor-row <- copy row
     156     *editor <- put *editor, cursor-row:offset, cursor-row
     157     cursor-column <- copy column
     158     *editor <- put *editor, cursor-column:offset, cursor-column
     159     before-cursor <- copy prev
     160     *editor <- put *editor, before-cursor:offset, before-cursor
     161   }
     162 ]
     163
     164 # Process an event 'e' and try to minimally update the screen.
     165 # Set 'go-render?' to true to indicate the caller must perform a non-minimal update.
     166 def handle-keyboard-event screen:&:screen, editor:&:editor, e:event -> go-render?:bool, screen:&:screen, editor:&:editor [
     167   local-scope
     168   load-inputs
     169   return-unless editor, false/don't-render
     170   screen-width:num <- screen-width screen
     171   screen-height:num <- screen-height screen
     172   left:num <- get *editor, left:offset
     173   right:num <- get *editor, right:offset
     174   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
     175   cursor-row:num <- get *editor, cursor-row:offset
     176   cursor-column:num <- get *editor, cursor-column:offset
     177   save-row:num <- copy cursor-row
     178   save-column:num <- copy cursor-column
     179   # character
     180   {
     181     c:char, is-unicode?:bool <- maybe-convert e, text:variant
     182     break-unless is-unicode?
     183     trace 10, [app], [handle-keyboard-event: special character]
     184     # exceptions for special characters go here
     185     <handle-special-character>
     186     # ignore any other special characters
     187     regular-character?:bool <- greater-or-equal c, 32/space
     188     return-unless regular-character?, false/don't-render
     189     # otherwise type it in
     190     <begin-insert-character>
     191     go-render? <- insert-at-cursor editor, c, screen
     192     <end-insert-character>
     193     return
     194   }
     195   # special key to modify the text or move the cursor
     196   k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
     197   assert is-keycode?, [event was of unknown type; neither keyboard nor mouse]
     198   # handlers for each special key will go here
     199   <handle-special-key>
     200   return true/go-render
     201 ]
     202
     203 def insert-at-cursor editor:&:editor, c:char, screen:&:screen -> go-render?:bool, editor:&:editor, screen:&:screen [
     204   local-scope
     205   load-inputs
     206   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
     207   insert c, before-cursor
     208   before-cursor <- next before-cursor
     209   *editor <- put *editor, before-cursor:offset, before-cursor
     210   cursor-row:num <- get *editor, cursor-row:offset
     211   cursor-column:num <- get *editor, cursor-column:offset
     212   left:num <- get *editor, left:offset
     213   right:num <- get *editor, right:offset
     214   save-row:num <- copy cursor-row
     215   save-column:num <- copy cursor-column
     216   screen-width:num <- screen-width screen
     217   screen-height:num <- screen-height screen
     218   # occasionally we'll need to mess with the cursor
     219   <insert-character-special-case>
     220   # but mostly we'll just move the cursor right
     221   cursor-column <- add cursor-column, 1
     222   *editor <- put *editor, cursor-column:offset, cursor-column
     223   next:&:duplex-list:char <- next before-cursor
     224   {
     225     # at end of all text? no need to scroll? just print the character and leave
     226     at-end?:bool <- equal next, null
     227     break-unless at-end?
     228     bottom:num <- subtract screen-height, 1
     229     at-bottom?:bool <- equal save-row, bottom
     230     at-right?:bool <- equal save-column, right
     231     overflow?:bool <- and at-bottom?, at-right?
     232     break-if overflow?
     233     move-cursor screen, save-row, save-column
     234     print screen, c
     235     return false/don't-render
     236   }
     237   {
     238     # not at right margin? print the character and rest of line
     239     break-unless next
     240     at-right?:bool <- greater-or-equal cursor-column, screen-width
     241     break-if at-right?
     242     curr:&:duplex-list:char <- copy before-cursor
     243     move-cursor screen, save-row, save-column
     244     curr-column:num <- copy save-column
     245     {
     246       # hit right margin? give up and let caller render
     247       at-right?:bool <- greater-than curr-column, right
     248       return-if at-right?, true/go-render
     249       break-unless curr
     250       # newline? done.
     251       currc:char <- get *curr, value:offset
     252       at-newline?:bool <- equal currc, 10/newline
     253       break-if at-newline?
     254       print screen, currc
     255       curr-column <- add curr-column, 1
     256       curr <- next curr
     257       loop
     258     }
     259     return false/don't-render
     260   }
     261   return true/go-render
     262 ]
     263
     264 # helper for tests
     265 def editor-render screen:&:screen, editor:&:editor -> screen:&:screen, editor:&:editor [
     266   local-scope
     267   load-inputs
     268   old-top-idx:num <- save-top-idx screen
     269   left:num <- get *editor, left:offset
     270   right:num <- get *editor, right:offset
     271   row:num, column:num <- render screen, editor
     272   draw-horizontal screen, row, left, right, 9480/horizontal-dotted
     273   row <- add row, 1
     274   clear-screen-from screen, row, left, left, right
     275   assert-no-scroll screen, old-top-idx
     276 ]
     277
     278 scenario editor-handles-empty-event-queue [
     279   local-scope
     280   assume-screen 10/width, 5/height
     281   e:&:editor <- new-editor [abc], 0/left, 10/right
     282   editor-render screen, e
     283   assume-console []
     284   run [
     285     editor-event-loop screen, console, e
     286   ]
     287   screen-should-contain [
     288     .          .
     289     .abc       .
     290     .╌╌╌╌╌╌╌╌╌╌.
     291     .          .
     292   ]
     293 ]
     294
     295 scenario editor-handles-mouse-clicks [
     296   local-scope
     297   assume-screen 10/width, 5/height
     298   e:&:editor <- new-editor [abc], 0/left, 10/right
     299   editor-render screen, e
     300   $clear-trace
     301   assume-console [
     302     left-click 1, 1  # on the 'b'
     303   ]
     304   run [
     305     editor-event-loop screen, console, e
     306     3:num/raw <- get *e, cursor-row:offset
     307     4:num/raw <- get *e, cursor-column:offset
     308   ]
     309   screen-should-contain [
     310     .          .
     311     .abc       .
     312     .╌╌╌╌╌╌╌╌╌╌.
     313     .          .
     314   ]
     315   memory-should-contain [
     316     3 <- 1  # cursor is at row 0..
     317     4 <- 1  # ..and column 1
     318   ]
     319   check-trace-count-for-label 0, [print-character]
     320 ]
     321
     322 scenario editor-handles-mouse-clicks-outside-text [
     323   local-scope
     324   assume-screen 10/width, 5/height
     325   e:&:editor <- new-editor [abc], 0/left, 10/right
     326   $clear-trace
     327   assume-console [
     328     left-click 1, 7  # last line, to the right of text
     329   ]
     330   run [
     331     editor-event-loop screen, console, e
     332     3:num/raw <- get *e, cursor-row:offset
     333     4:num/raw <- get *e, cursor-column:offset
     334   ]
     335   memory-should-contain [
     336     3 <- 1  # cursor row
     337     4 <- 3  # cursor column
     338   ]
     339   check-trace-count-for-label 0, [print-character]
     340 ]
     341
     342 scenario editor-handles-mouse-clicks-outside-text-2 [
     343   local-scope
     344   assume-screen 10/width, 5/height
     345   s:text <- new [abc
     346 def]
     347   e:&:editor <- new-editor s, 0/left, 10/right
     348   $clear-trace
     349   assume-console [
     350     left-click 1, 7  # interior line, to the right of text
     351   ]
     352   run [
     353     editor-event-loop screen, console, e
     354     3:num/raw <- get *e, cursor-row:offset
     355     4:num/raw <- get *e, cursor-column:offset
     356   ]
     357   memory-should-contain [
     358     3 <- 1  # cursor row
     359     4 <- 3  # cursor column
     360   ]
     361   check-trace-count-for-label 0, [print-character]
     362 ]
     363
     364 scenario editor-handles-mouse-clicks-outside-text-3 [
     365   local-scope
     366   assume-screen 10/width, 5/height
     367   s:text <- new [abc
     368 def]
     369   e:&:editor <- new-editor s, 0/left, 10/right
     370   $clear-trace
     371   assume-console [
     372     left-click 3, 7  # below text
     373   ]
     374   run [
     375     editor-event-loop screen, console, e
     376     3:num/raw <- get *e, cursor-row:offset
     377     4:num/raw <- get *e, cursor-column:offset
     378   ]
     379   memory-should-contain [
     380     3 <- 2  # cursor row
     381     4 <- 3  # cursor column
     382   ]
     383   check-trace-count-for-label 0, [print-character]
     384 ]
     385
     386 scenario editor-handles-mouse-clicks-outside-column [
     387   local-scope
     388   assume-screen 10/width, 5/height
     389   # editor occupies only left half of screen
     390   e:&:editor <- new-editor [abc], 0/left, 5/right
     391   editor-render screen, e
     392   $clear-trace
     393   assume-console [
     394     # click on right half of screen
     395     left-click 3, 8
     396   ]
     397   run [
     398     editor-event-loop screen, console, e
     399     3:num/raw <- get *e, cursor-row:offset
     400     4:num/raw <- get *e, cursor-column:offset
     401   ]
     402   screen-should-contain [
     403     .          .
     404     .abc       .
     405     .╌╌╌╌╌     .
     406     .          .
     407   ]
     408   memory-should-contain [
     409     3 <- 1  # no change to cursor row
     410     4 <- 0  # ..or column
     411   ]
     412   check-trace-count-for-label 0, [print-character]
     413 ]
     414
     415 scenario editor-handles-mouse-clicks-in-menu-area [
     416   local-scope
     417   assume-screen 10/width, 5/height
     418   e:&:editor <- new-editor [abc], 0/left, 5/right
     419   editor-render screen, e
     420   $clear-trace
     421   assume-console [
     422     # click on first, 'menu' row
     423     left-click 0, 3
     424   ]
     425   run [
     426     editor-event-loop screen, console, e
     427     3:num/raw <- get *e, cursor-row:offset
     428     4:num/raw <- get *e, cursor-column:offset
     429   ]
     430   # no change to cursor
     431   memory-should-contain [
     432     3 <- 1
     433     4 <- 0
     434   ]
     435 ]
     436
     437 scenario editor-inserts-characters-into-empty-editor [
     438   local-scope
     439   assume-screen 10/width, 5/height
     440   e:&:editor <- new-editor [], 0/left, 5/right
     441   editor-render screen, e
     442   $clear-trace
     443   assume-console [
     444     type [abc]
     445   ]
     446   run [
     447     editor-event-loop screen, console, e
     448   ]
     449   screen-should-contain [
     450     .          .
     451     .abc       .
     452     .╌╌╌╌╌     .
     453     .          .
     454   ]
     455   check-trace-count-for-label 3, [print-character]
     456 ]
     457
     458 scenario editor-inserts-characters-at-cursor [
     459   local-scope
     460   assume-screen 10/width, 5/height
     461   e:&:editor <- new-editor [abc], 0/left, 10/right
     462   editor-render screen, e
     463   $clear-trace
     464   # type two letters at different places
     465   assume-console [
     466     type [0]
     467     left-click 1, 2
     468     type [d]
     469   ]
     470   run [
     471     editor-event-loop screen, console, e
     472   ]
     473   screen-should-contain [
     474     .          .
     475     .0adbc     .
     476     .╌╌╌╌╌╌╌╌╌╌.
     477     .          .
     478   ]
     479   check-trace-count-for-label 7, [print-character]  # 4 for first letter, 3 for second
     480 ]
     481
     482 scenario editor-inserts-characters-at-cursor-2 [
     483   local-scope
     484   assume-screen 10/width, 5/height
     485   e:&:editor <- new-editor [abc], 0/left, 10/right
     486   editor-render screen, e
     487   $clear-trace
     488   assume-console [
     489     left-click 1, 5  # right of last line
     490     type [d]
     491   ]
     492   run [
     493     editor-event-loop screen, console, e
     494   ]
     495   screen-should-contain [
     496     .          .
     497     .abcd      .
     498     .╌╌╌╌╌╌╌╌╌╌.
     499     .          .
     500   ]
     501   check-trace-count-for-label 1, [print-character]
     502 ]
     503
     504 scenario editor-inserts-characters-at-cursor-5 [
     505   local-scope
     506   assume-screen 10/width, 5/height
     507   s:text <- new [abc
     508 d]
     509   e:&:editor <- new-editor s, 0/left, 10/right
     510   editor-render screen, e
     511   $clear-trace
     512   assume-console [
     513     left-click 1, 5  # right of non-last line
     514     type [e]
     515   ]
     516   run [
     517     editor-event-loop screen, console, e
     518   ]
     519   screen-should-contain [
     520     .          .
     521     .abce      .
     522     .d         .
     523     .╌╌╌╌╌╌╌╌╌╌.
     524     .          .
     525   ]
     526   check-trace-count-for-label 1, [print-character]
     527 ]
     528
     529 scenario editor-inserts-characters-at-cursor-3 [
     530   local-scope
     531   assume-screen 10/width, 5/height
     532   e:&:editor <- new-editor [abc], 0/left, 10/right
     533   editor-render screen, e
     534   $clear-trace
     535   assume-console [
     536     left-click 3, 5  # below all text
     537     type [d]
     538   ]
     539   run [
     540     editor-event-loop screen, console, e
     541   ]
     542   screen-should-contain [
     543     .          .
     544     .abcd      .
     545     .╌╌╌╌╌╌╌╌╌╌.
     546     .          .
     547   ]
     548   check-trace-count-for-label 1, [print-character]
     549 ]
     550
     551 scenario editor-inserts-characters-at-cursor-4 [
     552   local-scope
     553   assume-screen 10/width, 5/height
     554   s:text <- new [abc
     555 d]
     556   e:&:editor <- new-editor s, 0/left, 10/right
     557   editor-render screen, e
     558   $clear-trace
     559   assume-console [
     560     left-click 3, 5  # below all text
     561     type [e]
     562   ]
     563   run [
     564     editor-event-loop screen, console, e
     565   ]
     566   screen-should-contain [
     567     .          .
     568     .abc       .
     569     .de        .
     570     .╌╌╌╌╌╌╌╌╌╌.
     571     .          .
     572   ]
     573   check-trace-count-for-label 1, [print-character]
     574 ]
     575
     576 scenario editor-inserts-characters-at-cursor-6 [
     577   local-scope
     578   assume-screen 10/width, 5/height
     579   s:text <- new [abc
     580 d]
     581   e:&:editor <- new-editor s, 0/left, 10/right
     582   editor-render screen, e
     583   $clear-trace
     584   assume-console [
     585     left-click 3, 5  # below all text
     586     type [ef]
     587   ]
     588   run [
     589     editor-event-loop screen, console, e
     590   ]
     591   screen-should-contain [
     592     .          .
     593     .abc       .
     594     .def       .
     595     .╌╌╌╌╌╌╌╌╌╌.
     596     .          .
     597   ]
     598   check-trace-count-for-label 2, [print-character]
     599 ]
     600
     601 scenario editor-moves-cursor-after-inserting-characters [
     602   local-scope
     603   assume-screen 10/width, 5/height
     604   e:&:editor <- new-editor [ab], 0/left, 5/right
     605   editor-render screen, e
     606   assume-console [
     607     type [01]
     608   ]
     609   run [
     610     editor-event-loop screen, console, e
     611   ]
     612   screen-should-contain [
     613     .          .
     614     .01ab      .
     615     .╌╌╌╌╌     .
     616     .          .
     617   ]
     618 ]
     619
     620 # if the cursor reaches the right margin, wrap the line
     621
     622 scenario editor-wraps-line-on-insert [
     623   local-scope
     624   assume-screen 5/width, 5/height
     625   e:&:editor <- new-editor [abc], 0/left, 5/right
     626   editor-render screen, e
     627   # type a letter
     628   assume-console [
     629     type [e]
     630   ]
     631   run [
     632     editor-event-loop screen, console, e
     633   ]
     634   # no wrap yet
     635   screen-should-contain [
     636     .     .
     637     .eabc .
     638     .╌╌╌╌╌.
     639     .     .
     640     .     .
     641   ]
     642   # type a second letter
     643   assume-console [
     644     type [f]
     645   ]
     646   run [
     647     editor-event-loop screen, console, e
     648   ]
     649   # now wrap
     650   screen-should-contain [
     651     .     .
     652     .efab↩.
     653     .c    .
     654     .╌╌╌╌╌.
     655     .     .
     656   ]
     657 ]
     658
     659 scenario editor-wraps-line-on-insert-2 [
     660   local-scope
     661   # create an editor with some text
     662   assume-screen 10/width, 5/height
     663   s:text <- new [abcdefg
     664 defg]
     665   e:&:editor <- new-editor s, 0/left, 5/right
     666   editor-render screen, e
     667   # type more text at the start
     668   assume-console [
     669     left-click 3, 0
     670     type [abc]
     671   ]
     672   run [
     673     editor-event-loop screen, console, e
     674     3:num/raw <- get *e, cursor-row:offset
     675     4:num/raw <- get *e, cursor-column:offset
     676   ]
     677   # cursor is not wrapped
     678   memory-should-contain [
     679     3 <- 3
     680     4 <- 3
     681   ]
     682   # but line is wrapped
     683   screen-should-contain [
     684     .          .
     685     .abcd↩     .
     686     .efg       .
     687     .abcd↩     .
     688     .efg       .
     689   ]
     690 ]
     691
     692 after <insert-character-special-case> [
     693   # if the line wraps at the cursor, move cursor to start of next row
     694   {
     695     # if either:
     696     # a) we're at the end of the line and at the column of the wrap indicator, or
     697     # b) we're not at end of line and just before the column of the wrap indicator
     698     wrap-column:num <- copy right
     699     before-wrap-column:num <- subtract wrap-column, 1
     700     at-wrap?:bool <- greater-or-equal cursor-column, wrap-column
     701     just-before-wrap?:bool <- greater-or-equal cursor-column, before-wrap-column
     702     next:&:duplex-list:char <- next before-cursor
     703     # at end of line? next == 0 || next.value == 10/newline
     704     at-end-of-line?:bool <- equal next, null
     705     {
     706       break-if at-end-of-line?
     707       next-character:char <- get *next, value:offset
     708       at-end-of-line? <- equal next-character, 10/newline
     709     }
     710     # break unless ((eol? and at-wrap?) or (~eol? and just-before-wrap?))
     711     move-cursor-to-next-line?:bool <- copy false
     712     {
     713       break-if at-end-of-line?
     714       move-cursor-to-next-line? <- copy just-before-wrap?
     715       # if we're moving the cursor because it's in the middle of a wrapping
     716       # line, adjust it to left-most column
     717       potential-new-cursor-column:num <- copy left
     718     }
     719     {
     720       break-unless at-end-of-line?
     721       move-cursor-to-next-line? <- copy at-wrap?
     722       # if we're moving the cursor because it's at the end of a wrapping line,
     723       # adjust it to one past the left-most column to make room for the
     724       # newly-inserted wrap-indicator
     725       potential-new-cursor-column:num <- add left, 1/make-room-for-wrap-indicator
     726     }
     727     break-unless move-cursor-to-next-line?
     728     cursor-column <- copy potential-new-cursor-column
     729     *editor <- put *editor, cursor-column:offset, cursor-column
     730     cursor-row <- add cursor-row, 1
     731     *editor <- put *editor, cursor-row:offset, cursor-row
     732     # if we're out of the screen, scroll down
     733     {
     734       below-screen?:bool <- greater-or-equal cursor-row, screen-height
     735       break-unless below-screen?
     736       <scroll-down>
     737     }
     738     return true/go-render
     739   }
     740 ]
     741
     742 scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [
     743   local-scope
     744   assume-screen 10/width, 5/height
     745   e:&:editor <- new-editor [abcde], 0/left, 5/right
     746   assume-console [
     747     left-click 1, 3  # right before the wrap icon
     748     type [f]
     749   ]
     750   run [
     751     editor-event-loop screen, console, e
     752     3:num/raw <- get *e, cursor-row:offset
     753     4:num/raw <- get *e, cursor-column:offset
     754   ]
     755   screen-should-contain [
     756     .          .
     757     .abcf↩     .
     758     .de        .
     759     .╌╌╌╌╌     .
     760     .          .
     761   ]
     762   memory-should-contain [
     763     3 <- 2  # cursor row
     764     4 <- 0  # cursor column
     765   ]
     766 ]
     767
     768 scenario editor-wraps-cursor-after-inserting-characters-at-end-of-line [
     769   local-scope
     770   assume-screen 10/width, 5/height
     771   # create an editor containing two lines
     772   s:text <- new [abc
     773 xyz]
     774   e:&:editor <- new-editor s, 0/left, 5/right
     775   editor-render screen, e
     776   screen-should-contain [
     777     .          .
     778     .abc       .
     779     .xyz       .
     780     .╌╌╌╌╌     .
     781     .          .
     782   ]
     783   assume-console [
     784     left-click 1, 4  # at end of first line
     785     type [de]  # trigger wrap
     786   ]
     787   run [
     788     editor-event-loop screen, console, e
     789   ]
     790   screen-should-contain [
     791     .          .
     792     .abcd↩     .
     793     .e         .
     794     .xyz       .
     795     .╌╌╌╌╌     .
     796   ]
     797 ]
     798
     799 scenario editor-wraps-cursor-to-left-margin [
     800   local-scope
     801   assume-screen 10/width, 5/height
     802   e:&:editor <- new-editor [abcde], 2/left, 7/right
     803   assume-console [
     804     left-click 1, 5  # line is full; no wrap icon yet
     805     type [01]
     806   ]
     807   run [
     808     editor-event-loop screen, console, e
     809     3:num/raw <- get *e, cursor-row:offset
     810     4:num/raw <- get *e, cursor-column:offset
     811   ]
     812   screen-should-contain [
     813     .          .
     814     .  abc0↩   .
     815     .  1de     .
     816     .  ╌╌╌╌╌   .
     817     .          .
     818   ]
     819   memory-should-contain [
     820     3 <- 2  # cursor row
     821     4 <- 3  # cursor column
     822   ]
     823 ]
     824
     825 # if newline, move cursor to start of next line, and maybe align indent with previous line
     826
     827 container editor [
     828   indent?:bool
     829 ]
     830
     831 after <editor-initialization> [
     832   *result <- put *result, indent?:offset, true
     833 ]
     834
     835 scenario editor-moves-cursor-down-after-inserting-newline [
     836   local-scope
     837   assume-screen 10/width, 5/height
     838   e:&:editor <- new-editor [abc], 0/left, 10/right
     839   assume-console [
     840     type [0
     841 1]
     842   ]
     843   run [
     844     editor-event-loop screen, console, e
     845   ]
     846   screen-should-contain [
     847     .          .
     848     .0         .
     849     .1abc      .
     850     .╌╌╌╌╌╌╌╌╌╌.
     851     .          .
     852   ]
     853 ]
     854
     855 after <handle-special-character> [
     856   {
     857     newline?:bool <- equal c, 10/newline
     858     break-unless newline?
     859     <begin-insert-enter>
     860     insert-new-line-and-indent editor, screen
     861     <end-insert-enter>
     862     return true/go-render
     863   }
     864 ]
     865
     866 def insert-new-line-and-indent editor:&:editor, screen:&:screen -> editor:&:editor, screen:&:screen [
     867   local-scope
     868   load-inputs
     869   cursor-row:num <- get *editor, cursor-row:offset
     870   cursor-column:num <- get *editor, cursor-column:offset
     871   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
     872   left:num <- get *editor, left:offset
     873   right:num <- get *editor, right:offset
     874   screen-height:num <- screen-height screen
     875   # update cursor coordinates
     876   at-start-of-wrapped-line?:bool <- at-start-of-wrapped-line? editor
     877   {
     878     break-if at-start-of-wrapped-line?
     879     cursor-row <- add cursor-row, 1
     880     *editor <- put *editor, cursor-row:offset, cursor-row
     881   }
     882   cursor-column <- copy left
     883   *editor <- put *editor, cursor-column:offset, cursor-column
     884   # maybe scroll
     885   {
     886     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal, never greater
     887     break-unless below-screen?
     888     <scroll-down2>
     889     cursor-row <- subtract cursor-row, 1  # bring back into screen range
     890     *editor <- put *editor, cursor-row:offset, cursor-row
     891   }
     892   # insert newline
     893   insert 10/newline, before-cursor
     894   before-cursor <- next before-cursor
     895   *editor <- put *editor, before-cursor:offset, before-cursor
     896   # indent if necessary
     897   indent?:bool <- get *editor, indent?:offset
     898   return-unless indent?
     899   d:&:duplex-list:char <- get *editor, data:offset
     900   end-of-previous-line:&:duplex-list:char <- prev before-cursor
     901   indent:num <- line-indent end-of-previous-line, d
     902   i:num <- copy 0
     903   {
     904     indent-done?:bool <- greater-or-equal i, indent
     905     break-if indent-done?
     906     insert-at-cursor editor, 32/space, screen
     907     i <- add i, 1
     908     loop
     909   }
     910 ]
     911
     912 def at-start-of-wrapped-line? editor:&:editor -> result:bool [
     913   local-scope
     914   load-inputs
     915   left:num <- get *editor, left:offset
     916   cursor-column:num <- get *editor, cursor-column:offset
     917   cursor-at-left?:bool <- equal cursor-column, left
     918   return-unless cursor-at-left?, false
     919   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
     920   before-before-cursor:&:duplex-list:char <- prev before-cursor
     921   return-unless before-before-cursor, false  # cursor is at start of editor
     922   char-before-cursor:char <- get *before-cursor, value:offset
     923   cursor-after-newline?:bool <- equal char-before-cursor, 10/newline
     924   return-if cursor-after-newline?, false
     925   # if cursor is at left margin and not at start, but previous character is not a newline,
     926   # then we're at start of a wrapped line
     927   return true
     928 ]
     929
     930 # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
     931 # the number of spaces at the start of the line containing 'curr'.
     932 def line-indent curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
     933   local-scope
     934   load-inputs
     935   result:num <- copy 0
     936   return-unless curr
     937   at-start?:bool <- equal curr, start
     938   return-if at-start?
     939   {
     940     curr <- prev curr
     941     break-unless curr
     942     at-start?:bool <- equal curr, start
     943     break-if at-start?
     944     c:char <- get *curr, value:offset
     945     at-newline?:bool <- equal c, 10/newline
     946     break-if at-newline?
     947     # if c is a space, increment result
     948     is-space?:bool <- equal c, 32/space
     949     {
     950       break-unless is-space?
     951       result <- add result, 1
     952     }
     953     # if c is not a space, reset result
     954     {
     955       break-if is-space?
     956       result <- copy 0
     957     }
     958     loop
     959   }
     960 ]
     961
     962 scenario editor-moves-cursor-down-after-inserting-newline-2 [
     963   local-scope
     964   assume-screen 10/width, 5/height
     965   e:&:editor <- new-editor [abc], 1/left, 10/right
     966   assume-console [
     967     type [0
     968 1]
     969   ]
     970   run [
     971     editor-event-loop screen, console, e
     972   ]
     973   screen-should-contain [
     974     .          .
     975     . 0        .
     976     . 1abc     .
     977     . ╌╌╌╌╌╌╌╌╌.
     978     .          .
     979   ]
     980 ]
     981
     982 scenario editor-clears-previous-line-completely-after-inserting-newline [
     983   local-scope
     984   assume-screen 10/width, 5/height
     985   e:&:editor <- new-editor [abcde], 0/left, 5/right
     986   editor-render screen, e
     987   screen-should-contain [
     988     .          .
     989     .abcd↩     .
     990     .e         .
     991     .╌╌╌╌╌     .
     992     .          .
     993   ]
     994   assume-console [
     995     press enter
     996   ]
     997   run [
     998     editor-event-loop screen, console, e
     999   ]
    1000   # line should be fully cleared
    1001   screen-should-contain [
    1002     .          .
    1003     .          .
    1004     .abcd↩     .
    1005     .e         .
    1006     .╌╌╌╌╌     .
    1007   ]
    1008 ]
    1009
    1010 scenario editor-splits-wrapped-line-after-inserting-newline [
    1011   local-scope
    1012   assume-screen 10/width, 5/height
    1013   e:&:editor <- new-editor [abcdef], 0/left, 5/right
    1014   editor-render screen, e
    1015   screen-should-contain [
    1016     .          .
    1017     .abcd↩     .
    1018     .ef        .
    1019     .╌╌╌╌╌     .
    1020     .          .
    1021   ]
    1022   assume-console [
    1023     left-click 2, 0
    1024     press enter
    1025   ]
    1026   run [
    1027     editor-event-loop screen, console, e
    1028     10:num/raw <- get *e, cursor-row:offset
    1029     11:num/raw <- get *e, cursor-column:offset
    1030   ]
    1031   screen-should-contain [
    1032     .          .
    1033     .abcd      .
    1034     .ef        .
    1035     .╌╌╌╌╌     .
    1036   ]
    1037   memory-should-contain [
    1038     10 <- 2  # cursor-row
    1039     11 <- 0  # cursor-column
    1040   ]
    1041 ]
    1042
    1043 scenario editor-inserts-indent-after-newline [
    1044   local-scope
    1045   assume-screen 10/width, 10/height
    1046   s:text <- new [ab
    1047   cd
    1048 ef]
    1049   e:&:editor <- new-editor s, 0/left, 10/right
    1050   # position cursor after 'cd' and hit 'newline'
    1051   assume-console [
    1052     left-click 2, 8
    1053     type [
    1054 ]
    1055   ]
    1056   run [
    1057     editor-event-loop screen, console, e
    1058     3:num/raw <- get *e, cursor-row:offset
    1059     4:num/raw <- get *e, cursor-column:offset
    1060   ]
    1061   # cursor should be below start of previous line
    1062   memory-should-contain [
    1063     3 <- 3  # cursor row
    1064     4 <- 2  # cursor column (indented)
    1065   ]
    1066 ]
    1067
    1068 scenario editor-skips-indent-around-paste [
    1069   local-scope
    1070   assume-screen 10/width, 10/height
    1071   s:text <- new [ab
    1072   cd
    1073 ef]
    1074   e:&:editor <- new-editor s, 0/left, 10/right
    1075   # position cursor after 'cd' and hit 'newline' surrounded by paste markers
    1076   assume-console [
    1077     left-click 2, 8
    1078     press 65507  # start paste
    1079     press enter
    1080     press 65506  # end paste
    1081   ]
    1082   run [
    1083     editor-event-loop screen, console, e
    1084     3:num/raw <- get *e, cursor-row:offset
    1085     4:num/raw <- get *e, cursor-column:offset
    1086   ]
    1087   # cursor should be below start of previous line
    1088   memory-should-contain [
    1089     3 <- 3  # cursor row
    1090     4 <- 0  # cursor column (not indented)
    1091   ]
    1092 ]
    1093
    1094 after <handle-special-key> [
    1095   {
    1096     paste-start?:bool <- equal k, 65507/paste-start
    1097     break-unless paste-start?
    1098     *editor <- put *editor, indent?:offset, false
    1099     return true/go-render
    1100   }
    1101 ]
    1102
    1103 after <handle-special-key> [
    1104   {
    1105     paste-end?:bool <- equal k, 65506/paste-end
    1106     break-unless paste-end?
    1107     *editor <- put *editor, indent?:offset, true
    1108     return true/go-render
    1109   }
    1110 ]
    1111
    1112 ## helpers
    1113
    1114 def draw-horizontal screen:&:screen, row:num, x:num, right:num -> screen:&:screen [
    1115   local-scope
    1116   load-inputs
    1117   height:num <- screen-height screen
    1118   past-bottom?:bool <- greater-or-equal row, height
    1119   return-if past-bottom?
    1120   style:char, style-found?:bool <- next-input
    1121   {
    1122     break-if style-found?
    1123     style <- copy 9472/horizontal
    1124   }
    1125   color:num, color-found?:bool <- next-input
    1126   {
    1127     # default color to white
    1128     break-if color-found?
    1129     color <- copy 245/grey
    1130   }
    1131   bg-color:num, bg-color-found?:bool <- next-input
    1132   {
    1133     break-if bg-color-found?
    1134     bg-color <- copy 0/black
    1135   }
    1136   screen <- move-cursor screen, row, x
    1137   {
    1138     continue?:bool <- lesser-or-equal x, right  # right is inclusive, to match editor semantics
    1139     break-unless continue?
    1140     print screen, style, color, bg-color
    1141     x <- add x, 1
    1142     loop
    1143   }
    1144 ]