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 0/screen  # non-scrolling app
  10   editor:&:editor <- new-editor text, 5/left, 45/right
  11   editor-render 0/screen, editor
  12   editor-event-loop 0/screen, 0/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, 0/false
  54   click-row:num <- get t, row:offset
  55   return-unless click-row, 0/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?, 0/false
  60   right:num <- get *editor, right:offset
  61   too-far-right?:bool <- greater-than click-column, right
  62   return-if too-far-right?, 0/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 1/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, 0/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?, 0/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 1/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, 0/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 0/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?, 1/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 0/don't-render
 260   }
 261   return 1/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   clear-line-until screen, right
 273   row <- add row, 1
 274   draw-horizontal screen, row, left, right, 9480/horizontal-dotted
 275   row <- add row, 1
 276   clear-screen-from screen, row, left, left, right
 277   assert-no-scroll screen, old-top-idx
 278 ]
 279 
 280 scenario editor-handles-empty-event-queue [
 281   local-scope
 282   assume-screen 10/width, 5/height
 283   e:&:editor <- new-editor [abc], 0/left, 10/right
 284   editor-render screen, e
 285   assume-console []
 286   run [
 287     editor-event-loop screen, console, e
 288   ]
 289   screen-should-contain [
 290     .          .
 291     .abc       .
 292     .╌╌╌╌╌╌╌╌╌╌.
 293     .          .
 294   ]
 295 ]
 296 
 297 scenario editor-handles-mouse-clicks [
 298   local-scope
 299   assume-screen 10/width, 5/height
 300   e:&:editor <- new-editor [abc], 0/left, 10/right
 301   editor-render screen, e
 302   $clear-trace
 303   assume-console [
 304     left-click 1, 1  # on the 'b'
 305   ]
 306   run [
 307     editor-event-loop screen, console, e
 308     3:num/raw <- get *e, cursor-row:offset
 309     4:num/raw <- get *e, cursor-column:offset
 310   ]
 311   screen-should-contain [
 312     .          .
 313     .abc       .
 314     .╌╌╌╌╌╌╌╌╌╌.
 315     .          .
 316   ]
 317   memory-should-contain [
 318     3 <- 1  # cursor is at row 0..
 319     4 <- 1  # ..and column 1
 320   ]
 321   check-trace-count-for-label 0, [print-character]
 322 ]
 323 
 324 scenario editor-handles-mouse-clicks-outside-text [
 325   local-scope
 326   assume-screen 10/width, 5/height
 327   e:&:editor <- new-editor [abc], 0/left, 10/right
 328   $clear-trace
 329   assume-console [
 330     left-click 1, 7  # last line, to the right of text
 331   ]
 332   run [
 333     editor-event-loop screen, console, e
 334     3:num/raw <- get *e, cursor-row:offset
 335     4:num/raw <- get *e, cursor-column:offset
 336   ]
 337   memory-should-contain [
 338     3 <- 1  # cursor row
 339     4 <- 3  # cursor column
 340   ]
 341   check-trace-count-for-label 0, [print-character]
 342 ]
 343 
 344 scenario editor-handles-mouse-clicks-outside-text-2 [
 345   local-scope
 346   assume-screen 10/width, 5/height
 347   s:text <- new [abc
 348 def]
 349   e:&:editor <- new-editor s, 0/left, 10/right
 350   $clear-trace
 351   assume-console [
 352     left-click 1, 7  # interior line, to the right of text
 353   ]
 354   run [
 355     editor-event-loop screen, console, e
 356     3:num/raw <- get *e, cursor-row:offset
 357     4:num/raw <- get *e, cursor-column:offset
 358   ]
 359   memory-should-contain [
 360     3 <- 1  # cursor row
 361     4 <- 3  # cursor column
 362   ]
 363   check-trace-count-for-label 0, [print-character]
 364 ]
 365 
 366 scenario editor-handles-mouse-clicks-outside-text-3 [
 367   local-scope
 368   assume-screen 10/width, 5/height
 369   s:text <- new [abc
 370 def]
 371   e:&:editor <- new-editor s, 0/left, 10/right
 372   $clear-trace
 373   assume-console [
 374     left-click 3, 7  # below text
 375   ]
 376   run [
 377     editor-event-loop screen, console, e
 378     3:num/raw <- get *e, cursor-row:offset
 379     4:num/raw <- get *e, cursor-column:offset
 380   ]
 381   memory-should-contain [
 382     3 <- 2  # cursor row
 383     4 <- 3  # cursor column
 384   ]
 385   check-trace-count-for-label 0, [print-character]
 386 ]
 387 
 388 scenario editor-handles-mouse-clicks-outside-column [
 389   local-scope
 390   assume-screen 10/width, 5/height
 391   # editor occupies only left half of screen
 392   e:&:editor <- new-editor [abc], 0/left, 5/right
 393   editor-render screen, e
 394   $clear-trace
 395   assume-console [
 396     # click on right half of screen
 397     left-click 3, 8
 398   ]
 399   run [
 400     editor-event-loop screen, console, e
 401     3:num/raw <- get *e, cursor-row:offset
 402     4:num/raw <- get *e, cursor-column:offset
 403   ]
 404   screen-should-contain [
 405     .          .
 406     .abc       .
 407     .╌╌╌╌╌     .
 408     .          .
 409   ]
 410   memory-should-contain [
 411     3 <- 1  # no change to cursor row
 412     4 <- 0  # ..or column
 413   ]
 414   check-trace-count-for-label 0, [print-character]
 415 ]
 416 
 417 scenario editor-handles-mouse-clicks-in-menu-area [
 418   local-scope
 419   assume-screen 10/width, 5/height
 420   e:&:editor <- new-editor [abc], 0/left, 5/right
 421   editor-render screen, e
 422   $clear-trace
 423   assume-console [
 424     # click on first, 'menu' row
 425     left-click 0, 3
 426   ]
 427   run [
 428     editor-event-loop screen, console, e
 429     3:num/raw <- get *e, cursor-row:offset
 430     4:num/raw <- get *e, cursor-column:offset
 431   ]
 432   # no change to cursor
 433   memory-should-contain [
 434     3 <- 1
 435     4 <- 0
 436   ]
 437 ]
 438 
 439 scenario editor-inserts-characters-into-empty-editor [
 440   local-scope
 441   assume-screen 10/width, 5/height
 442   e:&:editor <- new-editor [], 0/left, 5/right
 443   editor-render screen, e
 444   $clear-trace
 445   assume-console [
 446     type [abc]
 447   ]
 448   run [
 449     editor-event-loop screen, console, e
 450   ]
 451   screen-should-contain [
 452     .          .
 453     .abc       .
 454     .╌╌╌╌╌     .
 455     .          .
 456   ]
 457   check-trace-count-for-label 3, [print-character]
 458 ]
 459 
 460 scenario editor-inserts-characters-at-cursor [
 461   local-scope
 462   assume-screen 10/width, 5/height
 463   e:&:editor <- new-editor [abc], 0/left, 10/right
 464   editor-render screen, e
 465   $clear-trace
 466   # type two letters at different places
 467   assume-console [
 468     type [0]
 469     left-click 1, 2
 470     type [d]
 471   ]
 472   run [
 473     editor-event-loop screen, console, e
 474   ]
 475   screen-should-contain [
 476     .          .
 477     .0adbc     .
 478     .╌╌╌╌╌╌╌╌╌╌.
 479     .          .
 480   ]
 481   check-trace-count-for-label 7, [print-character]  # 4 for first letter, 3 for second
 482 ]
 483 
 484 scenario editor-inserts-characters-at-cursor-2 [
 485   local-scope
 486   assume-screen 10/width, 5/height
 487   e:&:editor <- new-editor [abc], 0/left, 10/right
 488   editor-render screen, e
 489   $clear-trace
 490   assume-console [
 491     left-click 1, 5  # right of last line
 492     type [d]
 493   ]
 494   run [
 495     editor-event-loop screen, console, e
 496   ]
 497   screen-should-contain [
 498     .          .
 499     .abcd      .
 500     .╌╌╌╌╌╌╌╌╌╌.
 501     .          .
 502   ]
 503   check-trace-count-for-label 1, [print-character]
 504 ]
 505 
 506 scenario editor-inserts-characters-at-cursor-5 [
 507   local-scope
 508   assume-screen 10/width, 5/height
 509   s:text <- new [abc
 510 d]
 511   e:&:editor <- new-editor s, 0/left, 10/right
 512   editor-render screen, e
 513   $clear-trace
 514   assume-console [
 515     left-click 1, 5  # right of non-last line
 516     type [e]
 517   ]
 518   run [
 519     editor-event-loop screen, console, e
 520   ]
 521   screen-should-contain [
 522     .          .
 523     .abce      .
 524     .d         .
 525     .╌╌╌╌╌╌╌╌╌╌.
 526     .          .
 527   ]
 528   check-trace-count-for-label 1, [print-character]
 529 ]
 530 
 531 scenario editor-inserts-characters-at-cursor-3 [
 532   local-scope
 533   assume-screen 10/width, 5/height
 534   e:&:editor <- new-editor [abc], 0/left, 10/right
 535   editor-render screen, e
 536   $clear-trace
 537   assume-console [
 538     left-click 3, 5  # below all text
 539     type [d]
 540   ]
 541   run [
 542     editor-event-loop screen, console, e
 543   ]
 544   screen-should-contain [
 545     .          .
 546     .abcd      .
 547     .╌╌╌╌╌╌╌╌╌╌.
 548     .          .
 549   ]
 550   check-trace-count-for-label 1, [print-character]
 551 ]
 552 
 553 scenario editor-inserts-characters-at-cursor-4 [
 554   local-scope
 555   assume-screen 10/width, 5/height
 556   s:text <- new [abc
 557 d]
 558   e:&:editor <- new-editor s, 0/left, 10/right
 559   editor-render screen, e
 560   $clear-trace
 561   assume-console [
 562     left-click 3, 5  # below all text
 563     type [e]
 564   ]
 565   run [
 566     editor-event-loop screen, console, e
 567   ]
 568   screen-should-contain [
 569     .          .
 570     .abc       .
 571     .de        .
 572     .╌╌╌╌╌╌╌╌╌╌.
 573     .          .
 574   ]
 575   check-trace-count-for-label 1, [print-character]
 576 ]
 577 
 578 scenario editor-inserts-characters-at-cursor-6 [
 579   local-scope
 580   assume-screen 10/width, 5/height
 581   s:text <- new [abc
 582 d]
 583   e:&:editor <- new-editor s, 0/left, 10/right
 584   editor-render screen, e
 585   $clear-trace
 586   assume-console [
 587     left-click 3, 5  # below all text
 588     type [ef]
 589   ]
 590   run [
 591     editor-event-loop screen, console, e
 592   ]
 593   screen-should-contain [
 594     .          .
 595     .abc       .
 596     .def       .
 597     .╌╌╌╌╌╌╌╌╌╌.
 598     .          .
 599   ]
 600   check-trace-count-for-label 2, [print-character]
 601 ]
 602 
 603 scenario editor-moves-cursor-after-inserting-characters [
 604   local-scope
 605   assume-screen 10/width, 5/height
 606   e:&:editor <- new-editor [ab], 0/left, 5/right
 607   editor-render screen, e
 608   assume-console [
 609     type [01]
 610   ]
 611   run [
 612     editor-event-loop screen, console, e
 613   ]
 614   screen-should-contain [
 615     .          .
 616     .01ab      .
 617     .╌╌╌╌╌     .
 618     .          .
 619   ]
 620 ]
 621 
 622 # if the cursor reaches the right margin, wrap the line
 623 
 624 scenario editor-wraps-line-on-insert [
 625   local-scope
 626   assume-screen 5/width, 5/height
 627   e:&:editor <- new-editor [abc], 0/left, 5/right
 628   editor-render screen, e
 629   # type a letter
 630   assume-console [
 631     type [e]
 632   ]
 633   run [
 634     editor-event-loop screen, console, e
 635   ]
 636   # no wrap yet
 637   screen-should-contain [
 638     .     .
 639     .eabc .
 640     .╌╌╌╌╌.
 641     .     .
 642     .     .
 643   ]
 644   # type a second letter
 645   assume-console [
 646     type [f]
 647   ]
 648   run [
 649     editor-event-loop screen, console, e
 650   ]
 651   # now wrap
 652   screen-should-contain [
 653     .     .
 654     .efab↩.
 655     .c    .
 656     .╌╌╌╌╌.
 657     .     .
 658   ]
 659 ]
 660 
 661 scenario editor-wraps-line-on-insert-2 [
 662   local-scope
 663   # create an editor with some text
 664   assume-screen 10/width, 5/height
 665   s:text <- new [abcdefg
 666 defg]
 667   e:&:editor <- new-editor s, 0/left, 5/right
 668   editor-render screen, e
 669   # type more text at the start
 670   assume-console [
 671     left-click 3, 0
 672     type [abc]
 673   ]
 674   run [
 675     editor-event-loop screen, console, e
 676     3:num/raw <- get *e, cursor-row:offset
 677     4:num/raw <- get *e, cursor-column:offset
 678   ]
 679   # cursor is not wrapped
 680   memory-should-contain [
 681     3 <- 3
 682     4 <- 3
 683   ]
 684   # but line is wrapped
 685   screen-should-contain [
 686     .          .
 687     .abcd↩     .
 688     .efg       .
 689     .abcd↩     .
 690     .efg       .
 691   ]
 692 ]
 693 
 694 after <insert-character-special-case> [
 695   # if the line wraps at the cursor, move cursor to start of next row
 696   {
 697     # if either:
 698     # a) we're at the end of the line and at the column of the wrap indicator, or
 699     # b) we're not at end of line and just before the column of the wrap indicator
 700     wrap-column:num <- copy right
 701     before-wrap-column:num <- subtract wrap-column, 1
 702     at-wrap?:bool <- greater-or-equal cursor-column, wrap-column
 703     just-before-wrap?:bool <- greater-or-equal cursor-column, before-wrap-column
 704     next:&:duplex-list:char <- next before-cursor
 705     # at end of line? next == 0 || next.value == 10/newline
 706     at-end-of-line?:bool <- equal next, 0
 707     {
 708       break-if at-end-of-line?
 709       next-character:char <- get *next, value:offset
 710       at-end-of-line? <- equal next-character, 10/newline
 711     }
 712     # break unless ((eol? and at-wrap?) or (~eol? and just-before-wrap?))
 713     move-cursor-to-next-line?:bool <- copy 0/false
 714     {
 715       break-if at-end-of-line?
 716       move-cursor-to-next-line? <- copy just-before-wrap?
 717       # if we're moving the cursor because it's in the middle of a wrapping
 718       # line, adjust it to left-most column
 719       potential-new-cursor-column:num <- copy left
 720     }
 721     {
 722       break-unless at-end-of-line?
 723       move-cursor-to-next-line? <- copy at-wrap?
 724       # if we're moving the cursor because it's at the end of a wrapping line,
 725       # adjust it to one past the left-most column to make room for the
 726       # newly-inserted wrap-indicator
 727       potential-new-cursor-column:num <- add left, 1/make-room-for-wrap-indicator
 728     }
 729     break-unless move-cursor-to-next-line?
 730     cursor-column <- copy potential-new-cursor-column
 731     *editor <- put *editor, cursor-column:offset, cursor-column
 732     cursor-row <- add cursor-row, 1
 733     *editor <- put *editor, cursor-row:offset, cursor-row
 734     # if we're out of the screen, scroll down
 735     {
 736       below-screen?:bool <- greater-or-equal cursor-row, screen-height
 737       break-unless below-screen?
 738       <scroll-down>
 739     }
 740     return 1/go-render
 741   }
 742 ]
 743 
 744 scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [
 745   local-scope
 746   assume-screen 10/width, 5/height
 747   e:&:editor <- new-editor [abcde], 0/left, 5/right
 748   assume-console [
 749     left-click 1, 3  # right before the wrap icon
 750     type [f]
 751   ]
 752   run [
 753     editor-event-loop screen, console, e
 754     3:num/raw <- get *e, cursor-row:offset
 755     4:num/raw <- get *e, cursor-column:offset
 756   ]
 757   screen-should-contain [
 758     .          .
 759     .abcf↩     .
 760     .de        .
 761     .╌╌╌╌╌     .
 762     .          .
 763   ]
 764   memory-should-contain [
 765     3 <- 2  # cursor row
 766     4 <- 0  # cursor column
 767   ]
 768 ]
 769 
 770 scenario editor-wraps-cursor-after-inserting-characters-at-end-of-line [
 771   local-scope
 772   assume-screen 10/width, 5/height
 773   # create an editor containing two lines
 774   s:text <- new [abc
 775 xyz]
 776   e:&:editor <- new-editor s, 0/left, 5/right
 777   editor-render screen, e
 778   screen-should-contain [
 779     .          .
 780     .abc       .
 781     .xyz       .
 782     .╌╌╌╌╌     .
 783     .          .
 784   ]
 785   assume-console [
 786     left-click 1, 4  # at end of first line
 787     type [de]  # trigger wrap
 788   ]
 789   run [
 790     editor-event-loop screen, console, e
 791   ]
 792   screen-should-contain [
 793     .          .
 794     .abcd↩     .
 795     .e         .
 796     .xyz       .
 797     .╌╌╌╌╌     .
 798   ]
 799 ]
 800 
 801 scenario editor-wraps-cursor-to-left-margin [
 802   local-scope
 803   assume-screen 10/width, 5/height
 804   e:&:editor <- new-editor [abcde], 2/left, 7/right
 805   assume-console [
 806     left-click 1, 5  # line is full; no wrap icon yet
 807     type [01]
 808   ]
 809   run [
 810     editor-event-loop screen, console, e
 811     3:num/raw <- get *e, cursor-row:offset
 812     4:num/raw <- get *e, cursor-column:offset
 813   ]
 814   screen-should-contain [
 815     .          .
 816     .  abc0↩   .
 817     .  1de     .
 818     .  ╌╌╌╌╌   .
 819     .          .
 820   ]
 821   memory-should-contain [
 822     3 <- 2  # cursor row
 823     4 <- 3  # cursor column
 824   ]
 825 ]
 826 
 827 # if newline, move cursor to start of next line, and maybe align indent with previous line
 828 
 829 container editor [
 830   indent?:bool
 831 ]
 832 
 833 after <editor-initialization> [
 834   *result <- put *result, indent?:offset, 1/true
 835 ]
 836 
 837 scenario editor-moves-cursor-down-after-inserting-newline [
 838   local-scope
 839   assume-screen 10/width, 5/height
 840   e:&:editor <- new-editor [abc], 0/left, 10/right
 841   assume-console [
 842     type [0
 843 1]
 844   ]
 845   run [
 846     editor-event-loop screen, console, e
 847   ]
 848   screen-should-contain [
 849     .          .
 850     .0         .
 851     .1abc      .
 852     .╌╌╌╌╌╌╌╌╌╌.
 853     .          .
 854   ]
 855 ]
 856 
 857 after <handle-special-character> [
 858   {
 859     newline?:bool <- equal c, 10/newline
 860     break-unless newline?
 861     <begin-insert-enter>
 862     insert-new-line-and-indent editor, screen
 863     <end-insert-enter>
 864     return 1/go-render
 865   }
 866 ]
 867 
 868 def insert-new-line-and-indent editor:&:editor, screen:&:screen -> editor:&:editor, screen:&:screen [
 869   local-scope
 870   load-inputs
 871   cursor-row:num <- get *editor, cursor-row:offset
 872   cursor-column:num <- get *editor, cursor-column:offset
 873   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 874   left:num <- get *editor, left:offset
 875   right:num <- get *editor, right:offset
 876   screen-height:num <- screen-height screen
 877   # update cursor coordinates
 878   at-start-of-wrapped-line?:bool <- at-start-of-wrapped-line? editor
 879   {
 880     break-if at-start-of-wrapped-line?
 881     cursor-row <- add cursor-row, 1
 882     *editor <- put *editor, cursor-row:offset, cursor-row
 883   }
 884   cursor-column <- copy left
 885   *editor <- put *editor, cursor-column:offset, cursor-column
 886   # maybe scroll
 887   {
 888     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal, never greater
 889     break-unless below-screen?
 890     <scroll-down>
 891     cursor-row <- subtract cursor-row, 1  # bring back into screen range
 892     *editor <- put *editor, cursor-row:offset, cursor-row
 893   }
 894   # insert newline
 895   insert 10/newline, before-cursor
 896   before-cursor <- next before-cursor
 897   *editor <- put *editor, before-cursor:offset, before-cursor
 898   # indent if necessary
 899   indent?:bool <- get *editor, indent?:offset
 900   return-unless indent?
 901   d:&:duplex-list:char <- get *editor, data:offset
 902   end-of-previous-line:&:duplex-list:char <- prev before-cursor
 903   indent:num <- line-indent end-of-previous-line, d
 904   i:num <- copy 0
 905   {
 906     indent-done?:bool <- greater-or-equal i, indent
 907     break-if indent-done?
 908     insert-at-cursor editor, 32/space, screen
 909     i <- add i, 1
 910     loop
 911   }
 912 ]
 913 
 914 def at-start-of-wrapped-line? editor:&:editor -> result:bool [
 915   local-scope
 916   load-inputs
 917   left:num <- get *editor, left:offset
 918   cursor-column:num <- get *editor, cursor-column:offset
 919   cursor-at-left?:bool <- equal cursor-column, left
 920   return-unless cursor-at-left?, 0/false
 921   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 922   before-before-cursor:&:duplex-list:char <- prev before-cursor
 923   return-unless before-before-cursor, 0/false  # cursor is at start of editor
 924   char-before-cursor:char <- get *before-cursor, value:offset
 925   cursor-after-newline?:bool <- equal char-before-cursor, 10/newline
 926   return-if cursor-after-newline?, 0/false
 927   # if cursor is at left margin and not at start, but previous character is not a newline,
 928   # then we're at start of a wrapped line
 929   return 1/true
 930 ]
 931 
 932 # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
 933 # the number of spaces at the start of the line containing 'curr'.
 934 def line-indent curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
 935   local-scope
 936   load-inputs
 937   result:num <- copy 0
 938   return-unless curr
 939   at-start?:bool <- equal curr, start
 940   return-if at-start?
 941   {
 942     curr <- prev curr
 943     break-unless curr
 944     at-start?:bool <- equal curr, start
 945     break-if at-start?
 946     c:char <- get *curr, value:offset
 947     at-newline?:bool <- equal c, 10/newline
 948     break-if at-newline?
 949     # if c is a space, increment result
 950     is-space?:bool <- equal c, 32/space
 951     {
 952       break-unless is-space?
 953       result <- add result, 1
 954     }
 955     # if c is not a space, reset result
 956     {
 957       break-if is-space?
 958       result <- copy 0
 959     }
 960     loop
 961   }
 962 ]
 963 
 964 scenario editor-moves-cursor-down-after-inserting-newline-2 [
 965   local-scope
 966   assume-screen 10/width, 5/height
 967   e:&:editor <- new-editor [abc], 1/left, 10/right
 968   assume-console [
 969     type [0
 970 1]
 971   ]
 972   run [
 973     editor-event-loop screen, console, e
 974   ]
 975   screen-should-contain [
 976     .          .
 977     . 0        .
 978     . 1abc     .
 979     . ╌╌╌╌╌╌╌╌╌.
 980     .          .
 981   ]
 982 ]
 983 
 984 scenario editor-clears-previous-line-completely-after-inserting-newline [
 985   local-scope
 986   assume-screen 10/width, 5/height
 987   e:&:editor <- new-editor [abcde], 0/left, 5/right
 988   editor-render screen, e
 989   screen-should-contain [
 990     .          .
 991     .abcd↩     .
 992     .e         .
 993     .╌╌╌╌╌     .
 994     .          .
 995   ]
 996   assume-console [
 997     press enter
 998   ]
 999   run [
1000     editor-event-loop screen, console, e
1001   ]
1002   # line should be fully cleared
1003   screen-should-contain [
1004     .          .
1005     .          .
1006     .abcd↩     .
1007     .e         .
1008     .╌╌╌╌╌     .
1009   ]
1010 ]
1011 
1012 scenario editor-splits-wrapped-line-after-inserting-newline [
1013   local-scope
1014   assume-screen 10/width, 5/height
1015   e:&:editor <- new-editor [abcdef], 0/left, 5/right
1016   editor-render screen, e
1017   screen-should-contain [
1018     .          .
1019     .abcd↩     .
1020     .ef        .
1021     .╌╌╌╌╌     .
1022     .          .
1023   ]
1024   assume-console [
1025     left-click 2, 0
1026     press enter
1027   ]
1028   run [
1029     editor-event-loop screen, console, e
1030     10:num/raw <- get *e, cursor-row:offset
1031     11:num/raw <- get *e, cursor-column:offset
1032   ]
1033   screen-should-contain [
1034     .          .
1035     .abcd      .
1036     .ef        .
1037     .╌╌╌╌╌     .
1038   ]
1039   memory-should-contain [
1040     10 <- 2  # cursor-row
1041     11 <- 0  # cursor-column
1042   ]
1043 ]
1044 
1045 scenario editor-inserts-indent-after-newline [
1046   local-scope
1047   assume-screen 10/width, 10/height
1048   s:text <- new [ab
1049   cd
1050 ef]
1051   e:&:editor <- new-editor s, 0/left, 10/right
1052   # position cursor after 'cd' and hit 'newline'
1053   assume-console [
1054     left-click 2, 8
1055     type [
1056 ]
1057   ]
1058   run [
1059     editor-event-loop screen, console, e
1060     3:num/raw <- get *e, cursor-row:offset
1061     4:num/raw <- get *e, cursor-column:offset
1062   ]
1063   # cursor should be below start of previous line
1064   memory-should-contain [
1065     3 <- 3  # cursor row
1066     4 <- 2  # cursor column (indented)
1067   ]
1068 ]
1069 
1070 scenario editor-skips-indent-around-paste [
1071   local-scope
1072   assume-screen 10/width, 10/height
1073   s:text <- new [ab
1074   cd
1075 ef]
1076   e:&:editor <- new-editor s, 0/left, 10/right
1077   # position cursor after 'cd' and hit 'newline' surrounded by paste markers
1078   assume-console [
1079     left-click 2, 8
1080     press 65507  # start paste
1081     press enter
1082     press 65506  # end paste
1083   ]
1084   run [
1085     editor-event-loop screen, console, e
1086     3:num/raw <- get *e, cursor-row:offset
1087     4:num/raw <- get *e, cursor-column:offset
1088   ]
1089   # cursor should be below start of previous line
1090   memory-should-contain [
1091     3 <- 3  # cursor row
1092     4 <- 0  # cursor column (not indented)
1093   ]
1094 ]
1095 
1096 after <handle-special-key> [
1097   {
1098     paste-start?:bool <- equal k, 65507/paste-start
1099     break-unless paste-start?
1100     *editor <- put *editor, indent?:offset, 0/false
1101     return 1/go-render
1102   }
1103 ]
1104 
1105 after <handle-special-key> [
1106   {
1107     paste-end?:bool <- equal k, 65506/paste-end
1108     break-unless paste-end?
1109     *editor <- put *editor, indent?:offset, 1/true
1110     return 1/go-render
1111   }
1112 ]
1113 
1114 ## helpers
1115 
1116 def draw-horizontal screen:&:screen, row:num, x:num, right:num -> screen:&:screen [
1117   local-scope
1118   load-inputs
1119   height:num <- screen-height screen
1120   past-bottom?:bool <- greater-or-equal row, height
1121   return-if past-bottom?
1122   style:char, style-found?:bool <- next-input
1123   {
1124     break-if style-found?
1125     style <- copy 9472/horizontal
1126   }
1127   color:num, color-found?:bool <- next-input
1128   {
1129     # default color to white
1130     break-if color-found?
1131     color <- copy 245/grey
1132   }
1133   bg-color:num, bg-color-found?:bool <- next-input
1134   {
1135     break-if bg-color-found?
1136     bg-color <- copy 0/black
1137   }
1138   screen <- move-cursor screen, row, x
1139   {
1140     continue?:bool <- lesser-or-equal x, right  # right is inclusive, to match editor semantics
1141     break-unless continue?
1142     print screen, style, color, bg-color
1143     x <- add x, 1
1144     loop
1145   }
1146 ]