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 ]