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