1 ## special shortcuts for manipulating the editor
   2 # Some keys on the keyboard generate unicode characters, others generate
   3 # terminfo key codes. We need to modify different places in the two cases.
   4 
   5 # tab - insert two spaces
   6 
   7 scenario editor-inserts-two-spaces-on-tab [
   8   local-scope
   9   assume-screen 10/width, 5/height
  10   s:text <- new [ab
  11 cd]
  12   e:&:editor <- new-editor s, 0/left, 5/right
  13   editor-render screen, e
  14   $clear-trace
  15   assume-console [
  16     press tab
  17   ]
  18   run [
  19     editor-event-loop screen, console, e
  20   ]
  21   screen-should-contain [
  22     .          .
  23     .  ab      .
  24     .cd        .
  25   ]
  26   # we render at most two editor rows worth (one row for each space)
  27   check-trace-count-for-label-lesser-than 10, [print-character]
  28 ]
  29 
  30 scenario editor-inserts-two-spaces-and-wraps-line-on-tab [
  31   local-scope
  32   assume-screen 10/width, 5/height
  33   e:&:editor <- new-editor [abcd], 0/left, 5/right
  34   editor-render screen, e
  35   $clear-trace
  36   assume-console [
  37     press tab
  38   ]
  39   run [
  40     editor-event-loop screen, console, e
  41   ]
  42   screen-should-contain [
  43     .          .
  44     .  ab↩     .
  45     .cd        .
  46   ]
  47   # we re-render the whole editor
  48   check-trace-count-for-label-greater-than 10, [print-character]
  49 ]
  50 
  51 after <handle-special-character> [
  52   {
  53     tab?:bool <- equal c, 9/tab
  54     break-unless tab?
  55     <begin-insert-character>
  56     # todo: decompose insert-at-cursor into editor update and screen update,
  57     # so that 'tab' doesn't render the current line multiple times
  58     insert-at-cursor editor, 32/space, screen
  59     go-render? <- insert-at-cursor editor, 32/space, screen
  60     <end-insert-character>
  61     return
  62   }
  63 ]
  64 
  65 # backspace - delete character before cursor
  66 
  67 scenario editor-handles-backspace-key [
  68   local-scope
  69   assume-screen 10/width, 5/height
  70   e:&:editor <- new-editor [abc], 0/left, 10/right
  71   editor-render screen, e
  72   $clear-trace
  73   assume-console [
  74     left-click 1, 1
  75     press backspace
  76   ]
  77   run [
  78     editor-event-loop screen, console, e
  79     4:num/raw <- get *e, cursor-row:offset
  80     5:num/raw <- get *e, cursor-column:offset
  81   ]
  82   screen-should-contain [
  83     .          .
  84     .bc        .
  85     .╌╌╌╌╌╌╌╌╌╌.
  86     .          .
  87   ]
  88   memory-should-contain [
  89     4 <- 1
  90     5 <- 0
  91   ]
  92   check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
  93 ]
  94 
  95 after <handle-special-character> [
  96   {
  97     delete-previous-character?:bool <- equal c, 8/backspace
  98     break-unless delete-previous-character?
  99     <begin-backspace-character>
 100     go-render?:bool, backspaced-cell:&:duplex-list:char <- delete-before-cursor editor, screen
 101     <end-backspace-character>
 102     return
 103   }
 104 ]
 105 
 106 # return values:
 107 #   go-render? - whether caller needs to update the screen
 108 #   backspaced-cell - value deleted (or 0 if nothing was deleted) so we can save it for undo, etc.
 109 def delete-before-cursor editor:&:editor, screen:&:screen -> go-render?:bool, backspaced-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
 110   local-scope
 111   load-inputs
 112   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 113   data:&:duplex-list:char <- get *editor, data:offset
 114   # if at start of text (before-cursor at § sentinel), return
 115   prev:&:duplex-list:char <- prev before-cursor
 116   return-unless prev, 0/no-more-render, 0/nothing-deleted
 117   trace 10, [app], [delete-before-cursor]
 118   original-row:num <- get *editor, cursor-row:offset
 119   scroll?:bool <- move-cursor-coordinates-left editor
 120   backspaced-cell:&:duplex-list:char <- copy before-cursor
 121   data <- remove before-cursor, data  # will also neatly trim next/prev pointers in backspaced-cell/before-cursor
 122   before-cursor <- copy prev
 123   *editor <- put *editor, before-cursor:offset, before-cursor
 124   return-if scroll?, 1/go-render
 125   screen-width:num <- screen-width screen
 126   cursor-row:num <- get *editor, cursor-row:offset
 127   cursor-column:num <- get *editor, cursor-column:offset
 128   # did we just backspace over a newline?
 129   same-row?:bool <- equal cursor-row, original-row
 130   return-unless same-row?, 1/go-render
 131   left:num <- get *editor, left:offset
 132   right:num <- get *editor, right:offset
 133   curr:&:duplex-list:char <- next before-cursor
 134   screen <- move-cursor screen, cursor-row, cursor-column
 135   curr-column:num <- copy cursor-column
 136   {
 137     # hit right margin? give up and let caller render
 138     at-right?:bool <- greater-or-equal curr-column, right
 139     return-if at-right?, 1/go-render
 140     break-unless curr
 141     # newline? done.
 142     currc:char <- get *curr, value:offset
 143     at-newline?:bool <- equal currc, 10/newline
 144     break-if at-newline?
 145     screen <- print screen, currc
 146     curr-column <- add curr-column, 1
 147     curr <- next curr
 148     loop
 149   }
 150   # we're guaranteed not to be at the right margin
 151   space:char <- copy 32/space
 152   screen <- print screen, space
 153   go-render? <- copy 0/false
 154 ]
 155 
 156 def move-cursor-coordinates-left editor:&:editor -> go-render?:bool, editor:&:editor [
 157   local-scope
 158   load-inputs
 159   go-render?:bool <- copy 0/false
 160   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 161   cursor-row:num <- get *editor, cursor-row:offset
 162   cursor-column:num <- get *editor, cursor-column:offset
 163   left:num <- get *editor, left:offset
 164   # if not at left margin, move one character left
 165   {
 166     at-left-margin?:bool <- equal cursor-column, left
 167     break-if at-left-margin?
 168     trace 10, [app], [decrementing cursor column]
 169     cursor-column <- subtract cursor-column, 1
 170     *editor <- put *editor, cursor-column:offset, cursor-column
 171     return
 172   }
 173   # if at left margin, we must move to previous row:
 174   top-of-screen?:bool <- equal cursor-row, 1  # exclude menu bar
 175   {
 176     break-if top-of-screen?
 177     cursor-row <- subtract cursor-row, 1
 178     *editor <- put *editor, cursor-row:offset, cursor-row
 179   }
 180   {
 181     break-unless top-of-screen?
 182     <scroll-up>
 183     go-render? <- copy 1/true
 184   }
 185   {
 186     # case 1: if previous character was newline, figure out how long the previous line is
 187     previous-character:char <- get *before-cursor, value:offset
 188     previous-character-is-newline?:bool <- equal previous-character, 10/newline
 189     break-unless previous-character-is-newline?
 190     # compute length of previous line
 191     trace 10, [app], [switching to previous line]
 192     d:&:duplex-list:char <- get *editor, data:offset
 193     end-of-line:num <- previous-line-length before-cursor, d
 194     right:num <- get *editor, right:offset
 195     width:num <- subtract right, left
 196     wrap?:bool <- greater-than end-of-line, width
 197     {
 198       break-unless wrap?
 199       _, column-offset:num <- divide-with-remainder end-of-line, width
 200       cursor-column <- add left, column-offset
 201       *editor <- put *editor, cursor-column:offset, cursor-column
 202     }
 203     {
 204       break-if wrap?
 205       cursor-column <- add left, end-of-line
 206       *editor <- put *editor, cursor-column:offset, cursor-column
 207     }
 208     return
 209   }
 210   # case 2: if previous-character was not newline, we're just at a wrapped line
 211   trace 10, [app], [wrapping to previous line]
 212   right:num <- get *editor, right:offset
 213   cursor-column <- subtract right, 1  # leave room for wrap icon
 214   *editor <- put *editor, cursor-column:offset, cursor-column
 215 ]
 216 
 217 # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
 218 # the length of the previous line before the 'curr' pointer.
 219 def previous-line-length curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
 220   local-scope
 221   load-inputs
 222   result:num <- copy 0
 223   return-unless curr
 224   at-start?:bool <- equal curr, start
 225   return-if at-start?
 226   {
 227     curr <- prev curr
 228     break-unless curr
 229     at-start?:bool <- equal curr, start
 230     break-if at-start?
 231     c:char <- get *curr, value:offset
 232     at-newline?:bool <- equal c, 10/newline
 233     break-if at-newline?
 234     result <- add result, 1
 235     loop
 236   }
 237 ]
 238 
 239 scenario editor-clears-last-line-on-backspace [
 240   local-scope
 241   assume-screen 10/width, 5/height
 242   s:text <- new [ab
 243 cd]
 244   e:&:editor <- new-editor s, 0/left, 10/right
 245   assume-console [
 246     left-click 2, 0
 247     press backspace
 248   ]
 249   run [
 250     editor-event-loop screen, console, e
 251     4:num/raw <- get *e, cursor-row:offset
 252     5:num/raw <- get *e, cursor-column:offset
 253   ]
 254   screen-should-contain [
 255     .          .
 256     .abcd      .
 257     .╌╌╌╌╌╌╌╌╌╌.
 258     .          .
 259   ]
 260   memory-should-contain [
 261     4 <- 1
 262     5 <- 2
 263   ]
 264 ]
 265 
 266 scenario editor-joins-and-wraps-lines-on-backspace [
 267   local-scope
 268   assume-screen 10/width, 5/height
 269   # initialize editor with two long-ish but non-wrapping lines
 270   s:text <- new [abc def
 271 ghi jkl]
 272   e:&:editor <- new-editor s, 0/left, 10/right
 273   editor-render screen, e
 274   $clear-trace
 275   # position the cursor at the start of the second and hit backspace
 276   assume-console [
 277     left-click 2, 0
 278     press backspace
 279   ]
 280   run [
 281     editor-event-loop screen, console, e
 282   ]
 283   # resulting single line should wrap correctly
 284   screen-should-contain [
 285     .          .
 286     .abc defgh↩.
 287     .i jkl     .
 288     .╌╌╌╌╌╌╌╌╌╌.
 289     .          .
 290   ]
 291 ]
 292 
 293 scenario editor-wraps-long-lines-on-backspace [
 294   local-scope
 295   assume-screen 10/width, 5/height
 296   # initialize editor in part of the screen with a long line
 297   e:&:editor <- new-editor [abc def ghij], 0/left, 8/right
 298   editor-render screen, e
 299   # confirm that it wraps
 300   screen-should-contain [
 301     .          .
 302     .abc def↩  .
 303     . ghij     .
 304     .╌╌╌╌╌╌╌╌  .
 305   ]
 306   $clear-trace
 307   # position the cursor somewhere in the middle of the top screen line and hit backspace
 308   assume-console [
 309     left-click 1, 4
 310     press backspace
 311   ]
 312   run [
 313     editor-event-loop screen, console, e
 314   ]
 315   # resulting single line should wrap correctly and not overflow its bounds
 316   screen-should-contain [
 317     .          .
 318     .abcdef ↩  .
 319     .ghij      .
 320     .╌╌╌╌╌╌╌╌  .
 321     .          .
 322   ]
 323 ]
 324 
 325 # delete - delete character at cursor
 326 
 327 scenario editor-handles-delete-key [
 328   local-scope
 329   assume-screen 10/width, 5/height
 330   e:&:editor <- new-editor [abc], 0/left, 10/right
 331   editor-render screen, e
 332   $clear-trace
 333   assume-console [
 334     press delete
 335   ]
 336   run [
 337     editor-event-loop screen, console, e
 338   ]
 339   screen-should-contain [
 340     .          .
 341     .bc        .
 342     .╌╌╌╌╌╌╌╌╌╌.
 343     .          .
 344   ]
 345   check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
 346   $clear-trace
 347   assume-console [
 348     press delete
 349   ]
 350   run [
 351     editor-event-loop screen, console, e
 352   ]
 353   screen-should-contain [
 354     .          .
 355     .c         .
 356     .╌╌╌╌╌╌╌╌╌╌.
 357     .          .
 358   ]
 359   check-trace-count-for-label 2, [print-character]  # new length to overwrite
 360 ]
 361 
 362 after <handle-special-key> [
 363   {
 364     delete-next-character?:bool <- equal k, 65522/delete
 365     break-unless delete-next-character?
 366     <begin-delete-character>
 367     go-render?:bool, deleted-cell:&:duplex-list:char <- delete-at-cursor editor, screen
 368     <end-delete-character>
 369     return
 370   }
 371 ]
 372 
 373 def delete-at-cursor editor:&:editor, screen:&:screen -> go-render?:bool, deleted-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
 374   local-scope
 375   load-inputs
 376   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 377   data:&:duplex-list:char <- get *editor, data:offset
 378   deleted-cell:&:duplex-list:char <- next before-cursor
 379   return-unless deleted-cell, 0/don't-render
 380   currc:char <- get *deleted-cell, value:offset
 381   data <- remove deleted-cell, data
 382   deleted-newline?:bool <- equal currc, 10/newline
 383   return-if deleted-newline?, 1/go-render
 384   # wasn't a newline? render rest of line
 385   curr:&:duplex-list:char <- next before-cursor  # refresh after remove above
 386   cursor-row:num <- get *editor, cursor-row:offset
 387   cursor-column:num <- get *editor, cursor-column:offset
 388   screen <- move-cursor screen, cursor-row, cursor-column
 389   curr-column:num <- copy cursor-column
 390   screen-width:num <- screen-width screen
 391   {
 392     # hit right margin? give up and let caller render
 393     at-right?:bool <- greater-or-equal curr-column, screen-width
 394     return-if at-right?, 1/go-render
 395     break-unless curr
 396     currc:char <- get *curr, value:offset
 397     at-newline?:bool <- equal currc, 10/newline
 398     break-if at-newline?
 399     screen <- print screen, currc
 400     curr-column <- add curr-column, 1
 401     curr <- next curr
 402     loop
 403   }
 404   # we're guaranteed not to be at the right margin
 405   space:char <- copy 32/space
 406   screen <- print screen, space
 407   go-render? <- copy 0/false
 408 ]
 409 
 410 # right arrow
 411 
 412 scenario editor-moves-cursor-right-with-key [
 413   local-scope
 414   assume-screen 10/width, 5/height
 415   e:&:editor <- new-editor [abc], 0/left, 10/right
 416   editor-render screen, e
 417   $clear-trace
 418   assume-console [
 419     press right-arrow
 420     type [0]
 421   ]
 422   run [
 423     editor-event-loop screen, console, e
 424   ]
 425   screen-should-contain [
 426     .          .
 427     .a0bc      .
 428     .╌╌╌╌╌╌╌╌╌╌.
 429     .          .
 430   ]
 431   check-trace-count-for-label 3, [print-character]  # 0 and following characters
 432 ]
 433 
 434 after <handle-special-key> [
 435   {
 436     move-to-next-character?:bool <- equal k, 65514/right-arrow
 437     break-unless move-to-next-character?
 438     # if not at end of text
 439     next-cursor:&:duplex-list:char <- next before-cursor
 440     break-unless next-cursor
 441     # scan to next character
 442     <begin-move-cursor>
 443     before-cursor <- copy next-cursor
 444     *editor <- put *editor, before-cursor:offset, before-cursor
 445     go-render?:bool <- move-cursor-coordinates-right editor, screen-height
 446     screen <- move-cursor screen, cursor-row, cursor-column
 447     undo-coalesce-tag:num <- copy 2/right-arrow
 448     <end-move-cursor>
 449     return
 450   }
 451 ]
 452 
 453 def move-cursor-coordinates-right editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
 454   local-scope
 455   load-inputs
 456   before-cursor:&:duplex-list:char <- get *editor before-cursor:offset
 457   cursor-row:num <- get *editor, cursor-row:offset
 458   cursor-column:num <- get *editor, cursor-column:offset
 459   left:num <- get *editor, left:offset
 460   right:num <- get *editor, right:offset
 461   # if crossed a newline, move cursor to start of next row
 462   {
 463     old-cursor-character:char <- get *before-cursor, value:offset
 464     was-at-newline?:bool <- equal old-cursor-character, 10/newline
 465     break-unless was-at-newline?
 466     cursor-row <- add cursor-row, 1
 467     *editor <- put *editor, cursor-row:offset, cursor-row
 468     cursor-column <- copy left
 469     *editor <- put *editor, cursor-column:offset, cursor-column
 470     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
 471     return-unless below-screen?, 0/don't-render
 472     <scroll-down>
 473     cursor-row <- subtract cursor-row, 1  # bring back into screen range
 474     *editor <- put *editor, cursor-row:offset, cursor-row
 475     return 1/go-render
 476   }
 477   # if the line wraps, move cursor to start of next row
 478   {
 479     # if we're at the column just before the wrap indicator
 480     wrap-column:num <- subtract right, 1
 481     at-wrap?:bool <- equal cursor-column, wrap-column
 482     break-unless at-wrap?
 483     # and if next character isn't newline
 484     next:&:duplex-list:char <- next before-cursor
 485     break-unless next
 486     next-character:char <- get *next, value:offset
 487     newline?:bool <- equal next-character, 10/newline
 488     break-if newline?
 489     cursor-row <- add cursor-row, 1
 490     *editor <- put *editor, cursor-row:offset, cursor-row
 491     cursor-column <- copy left
 492     *editor <- put *editor, cursor-column:offset, cursor-column
 493     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
 494     return-unless below-screen?, 0/no-more-render
 495     <scroll-down>
 496     cursor-row <- subtract cursor-row, 1  # bring back into screen range
 497     *editor <- put *editor, cursor-row:offset, cursor-row
 498     return 1/go-render
 499   }
 500   # otherwise move cursor one character right
 501   cursor-column <- add cursor-column, 1
 502   *editor <- put *editor, cursor-column:offset, cursor-column
 503   go-render? <- copy 0/false
 504 ]
 505 
 506 scenario editor-moves-cursor-to-next-line-with-right-arrow [
 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   # type right-arrow a few times to get to start of second line
 515   assume-console [
 516     press right-arrow
 517     press right-arrow
 518     press right-arrow
 519     press right-arrow  # next line
 520   ]
 521   run [
 522     editor-event-loop screen, console, e
 523   ]
 524   check-trace-count-for-label 0, [print-character]
 525   # type something and ensure it goes where it should
 526   assume-console [
 527     type [0]
 528   ]
 529   run [
 530     editor-event-loop screen, console, e
 531   ]
 532   screen-should-contain [
 533     .          .
 534     .abc       .
 535     .0d        .
 536     .╌╌╌╌╌╌╌╌╌╌.
 537     .          .
 538   ]
 539   check-trace-count-for-label 2, [print-character]  # new length of second line
 540 ]
 541 
 542 scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [
 543   local-scope
 544   assume-screen 10/width, 5/height
 545   s:text <- new [abc
 546 d]
 547   e:&:editor <- new-editor s, 1/left, 10/right
 548   editor-render screen, e
 549   assume-console [
 550     press right-arrow
 551     press right-arrow
 552     press right-arrow
 553     press right-arrow  # next line
 554     type [0]
 555   ]
 556   run [
 557     editor-event-loop screen, console, e
 558   ]
 559   screen-should-contain [
 560     .          .
 561     . abc      .
 562     . 0d       .
 563     . ╌╌╌╌╌╌╌╌╌.
 564     .          .
 565   ]
 566 ]
 567 
 568 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [
 569   local-scope
 570   assume-screen 10/width, 5/height
 571   e:&:editor <- new-editor [abcdef], 0/left, 5/right
 572   editor-render screen, e
 573   $clear-trace
 574   assume-console [
 575     left-click 1, 3
 576     press right-arrow
 577   ]
 578   run [
 579     editor-event-loop screen, console, e
 580     3:num/raw <- get *e, cursor-row:offset
 581     4:num/raw <- get *e, cursor-column:offset
 582   ]
 583   screen-should-contain [
 584     .          .
 585     .abcd↩     .
 586     .ef        .
 587     .╌╌╌╌╌     .
 588     .          .
 589   ]
 590   memory-should-contain [
 591     3 <- 2
 592     4 <- 0
 593   ]
 594   check-trace-count-for-label 0, [print-character]
 595 ]
 596 
 597 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [
 598   local-scope
 599   assume-screen 10/width, 5/height
 600   # line just barely wrapping
 601   e:&:editor <- new-editor [abcde], 0/left, 5/right
 602   editor-render screen, e
 603   $clear-trace
 604   # position cursor at last character before wrap and hit right-arrow
 605   assume-console [
 606     left-click 1, 3
 607     press right-arrow
 608   ]
 609   run [
 610     editor-event-loop screen, console, e
 611     3:num/raw <- get *e, cursor-row:offset
 612     4:num/raw <- get *e, cursor-column:offset
 613   ]
 614   memory-should-contain [
 615     3 <- 2
 616     4 <- 0
 617   ]
 618   # now hit right arrow again
 619   assume-console [
 620     press right-arrow
 621   ]
 622   run [
 623     editor-event-loop screen, console, e
 624     3:num/raw <- get *e, cursor-row:offset
 625     4:num/raw <- get *e, cursor-column:offset
 626   ]
 627   memory-should-contain [
 628     3 <- 2
 629     4 <- 1
 630   ]
 631   check-trace-count-for-label 0, [print-character]
 632 ]
 633 
 634 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [
 635   local-scope
 636   assume-screen 10/width, 5/height
 637   e:&:editor <- new-editor [abcdef], 1/left, 6/right
 638   editor-render screen, e
 639   $clear-trace
 640   assume-console [
 641     left-click 1, 4
 642     press right-arrow
 643   ]
 644   run [
 645     editor-event-loop screen, console, e
 646     3:num/raw <- get *e, cursor-row:offset
 647     4:num/raw <- get *e, cursor-column:offset
 648   ]
 649   screen-should-contain [
 650     .          .
 651     . abcd↩    .
 652     . ef       .
 653     . ╌╌╌╌╌    .
 654     .          .
 655   ]
 656   memory-should-contain [
 657     3 <- 2
 658     4 <- 1
 659   ]
 660   check-trace-count-for-label 0, [print-character]
 661 ]
 662 
 663 scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [
 664   local-scope
 665   assume-screen 10/width, 5/height
 666   s:text <- new [abc
 667 d]
 668   e:&:editor <- new-editor s, 0/left, 10/right
 669   editor-render screen, e
 670   $clear-trace
 671   # move to end of line, press right-arrow, type a character
 672   assume-console [
 673     left-click 1, 3
 674     press right-arrow
 675     type [0]
 676   ]
 677   run [
 678     editor-event-loop screen, console, e
 679   ]
 680   # new character should be in next line
 681   screen-should-contain [
 682     .          .
 683     .abc       .
 684     .0d        .
 685     .╌╌╌╌╌╌╌╌╌╌.
 686     .          .
 687   ]
 688   check-trace-count-for-label 2, [print-character]
 689 ]
 690 
 691 # todo: ctrl-right: next word-end
 692 
 693 # left arrow
 694 
 695 scenario editor-moves-cursor-left-with-key [
 696   local-scope
 697   assume-screen 10/width, 5/height
 698   e:&:editor <- new-editor [abc], 0/left, 10/right
 699   editor-render screen, e
 700   $clear-trace
 701   assume-console [
 702     left-click 1, 2
 703     press left-arrow
 704     type [0]
 705   ]
 706   run [
 707     editor-event-loop screen, console, e
 708   ]
 709   screen-should-contain [
 710     .          .
 711     .a0bc      .
 712     .╌╌╌╌╌╌╌╌╌╌.
 713     .          .
 714   ]
 715   check-trace-count-for-label 3, [print-character]
 716 ]
 717 
 718 after <handle-special-key> [
 719   {
 720     move-to-previous-character?:bool <- equal k, 65515/left-arrow
 721     break-unless move-to-previous-character?
 722     trace 10, [app], [left arrow]
 723     # if not at start of text (before-cursor at § sentinel)
 724     prev:&:duplex-list:char <- prev before-cursor
 725     return-unless prev, 0/don't-render
 726     <begin-move-cursor>
 727     go-render? <- move-cursor-coordinates-left editor
 728     before-cursor <- copy prev
 729     *editor <- put *editor, before-cursor:offset, before-cursor
 730     undo-coalesce-tag:num <- copy 1/left-arrow
 731     <end-move-cursor>
 732     return
 733   }
 734 ]
 735 
 736 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [
 737   local-scope
 738   assume-screen 10/width, 5/height
 739   # initialize editor with two lines
 740   s:text <- new [abc
 741 d]
 742   e:&:editor <- new-editor s, 0/left, 10/right
 743   editor-render screen, e
 744   $clear-trace
 745   # position cursor at start of second line (so there's no previous newline)
 746   assume-console [
 747     left-click 2, 0
 748     press left-arrow
 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   memory-should-contain [
 756     3 <- 1
 757     4 <- 3
 758   ]
 759   check-trace-count-for-label 0, [print-character]
 760 ]
 761 
 762 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [
 763   local-scope
 764   assume-screen 10/width, 5/height
 765   # initialize editor with three lines
 766   s:text <- new [abc
 767 def
 768 g]
 769   e:&:editor <- new-editor s:text, 0/left, 10/right
 770   editor-render screen, e
 771   $clear-trace
 772   # position cursor further down (so there's a newline before the character at
 773   # the cursor)
 774   assume-console [
 775     left-click 3, 0
 776     press left-arrow
 777     type [0]
 778   ]
 779   run [
 780     editor-event-loop screen, console, e
 781   ]
 782   screen-should-contain [
 783     .          .
 784     .abc       .
 785     .def0      .
 786     .g         .
 787     .╌╌╌╌╌╌╌╌╌╌.
 788   ]
 789   check-trace-count-for-label 1, [print-character]  # just the '0'
 790 ]
 791 
 792 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [
 793   local-scope
 794   assume-screen 10/width, 5/height
 795   s:text <- new [abc
 796 def
 797 g]
 798   e:&:editor <- new-editor s, 0/left, 10/right
 799   editor-render screen, e
 800   $clear-trace
 801   # position cursor at start of text, press left-arrow, then type a character
 802   assume-console [
 803     left-click 1, 0
 804     press left-arrow
 805     type [0]
 806   ]
 807   run [
 808     editor-event-loop screen, console, e
 809   ]
 810   # left-arrow should have had no effect
 811   screen-should-contain [
 812     .          .
 813     .0abc      .
 814     .def       .
 815     .g         .
 816     .╌╌╌╌╌╌╌╌╌╌.
 817   ]
 818   check-trace-count-for-label 4, [print-character]  # length of first line
 819 ]
 820 
 821 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [
 822   local-scope
 823   assume-screen 10/width, 5/height
 824   # initialize editor with text containing an empty line
 825   s:text <- new [abc
 826 
 827 d]
 828   e:&:editor <- new-editor s, 0/left, 10/right
 829   editor-render screen, e:&:editor
 830   $clear-trace
 831   # position cursor right after empty line
 832   assume-console [
 833     left-click 3, 0
 834     press left-arrow
 835     type [0]
 836   ]
 837   run [
 838     editor-event-loop screen, console, e
 839   ]
 840   screen-should-contain [
 841     .          .
 842     .abc       .
 843     .0         .
 844     .d         .
 845     .╌╌╌╌╌╌╌╌╌╌.
 846   ]
 847   check-trace-count-for-label 1, [print-character]  # just the '0'
 848 ]
 849 
 850 scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [
 851   local-scope
 852   assume-screen 10/width, 5/height
 853   # initialize editor with a wrapping line
 854   e:&:editor <- new-editor [abcdef], 0/left, 5/right
 855   editor-render screen, e
 856   $clear-trace
 857   screen-should-contain [
 858     .          .
 859     .abcd↩     .
 860     .ef        .
 861     .╌╌╌╌╌     .
 862     .          .
 863   ]
 864   # position cursor right after empty line
 865   assume-console [
 866     left-click 2, 0
 867     press left-arrow
 868   ]
 869   run [
 870     editor-event-loop screen, console, e
 871     3:num/raw <- get *e, cursor-row:offset
 872     4:num/raw <- get *e, cursor-column:offset
 873   ]
 874   memory-should-contain [
 875     3 <- 1  # previous row
 876     4 <- 3  # right margin except wrap icon
 877   ]
 878   check-trace-count-for-label 0, [print-character]
 879 ]
 880 
 881 scenario editor-moves-across-screen-lines-to-wrapping-line-with-left-arrow [
 882   local-scope
 883   assume-screen 10/width, 5/height
 884   # initialize editor with a wrapping line followed by a second line
 885   s:text <- new [abcdef
 886 g]
 887   e:&:editor <- new-editor s, 0/left, 5/right
 888   editor-render screen, e
 889   $clear-trace
 890   screen-should-contain [
 891     .          .
 892     .abcd↩     .
 893     .ef        .
 894     .g         .
 895     .╌╌╌╌╌     .
 896   ]
 897   # position cursor right after empty line
 898   assume-console [
 899     left-click 3, 0
 900     press left-arrow
 901   ]
 902   run [
 903     editor-event-loop screen, console, e
 904     3:num/raw <- get *e, cursor-row:offset
 905     4:num/raw <- get *e, cursor-column:offset
 906   ]
 907   memory-should-contain [
 908     3 <- 2  # previous row
 909     4 <- 2  # end of wrapped line
 910   ]
 911   check-trace-count-for-label 0, [print-character]
 912 ]
 913 
 914 scenario editor-moves-across-screen-lines-to-non-wrapping-line-with-left-arrow [
 915   local-scope
 916   assume-screen 10/width, 5/height
 917   # initialize editor with a line on the verge of wrapping, followed by a second line
 918   s:text <- new [abcd
 919 e]
 920   e:&:editor <- new-editor s, 0/left, 5/right
 921   editor-render screen, e
 922   $clear-trace
 923   screen-should-contain [
 924     .          .
 925     .abcd      .
 926     .e         .
 927     .╌╌╌╌╌     .
 928     .          .
 929   ]
 930   # position cursor right after empty line
 931   assume-console [
 932     left-click 2, 0
 933     press left-arrow
 934   ]
 935   run [
 936     editor-event-loop screen, console, e
 937     3:num/raw <- get *e, cursor-row:offset
 938     4:num/raw <- get *e, cursor-column:offset
 939   ]
 940   memory-should-contain [
 941     3 <- 1  # previous row
 942     4 <- 4  # end of wrapped line
 943   ]
 944   check-trace-count-for-label 0, [print-character]
 945 ]
 946 
 947 # todo: ctrl-left: previous word-start
 948 
 949 # up arrow
 950 
 951 scenario editor-moves-to-previous-line-with-up-arrow [
 952   local-scope
 953   assume-screen 10/width, 5/height
 954   s:text <- new [abc
 955 def]
 956   e:&:editor <- new-editor s, 0/left, 10/right
 957   editor-render screen, e
 958   $clear-trace
 959   assume-console [
 960     left-click 2, 1
 961     press up-arrow
 962   ]
 963   run [
 964     editor-event-loop screen, console, e
 965     3:num/raw <- get *e, cursor-row:offset
 966     4:num/raw <- get *e, cursor-column:offset
 967   ]
 968   memory-should-contain [
 969     3 <- 1
 970     4 <- 1
 971   ]
 972   check-trace-count-for-label 0, [print-character]
 973   assume-console [
 974     type [0]
 975   ]
 976   run [
 977     editor-event-loop screen, console, e
 978   ]
 979   screen-should-contain [
 980     .          .
 981     .a0bc      .
 982     .def       .
 983     .╌╌╌╌╌╌╌╌╌╌.
 984     .          .
 985   ]
 986 ]
 987 
 988 after <handle-special-key> [
 989   {
 990     move-to-previous-line?:bool <- equal k, 65517/up-arrow
 991     break-unless move-to-previous-line?
 992     <begin-move-cursor>
 993     go-render? <- move-to-previous-line editor
 994     undo-coalesce-tag:num <- copy 3/up-arrow
 995     <end-move-cursor>
 996     return
 997   }
 998 ]
 999 
1000 def move-to-previous-line editor:&:editor -> go-render?:bool, editor:&:editor [
1001   local-scope
1002   load-inputs
1003   go-render?:bool <- copy 0/false
1004   cursor-row:num <- get *editor, cursor-row:offset
1005   cursor-column:num <- get *editor, cursor-column:offset
1006   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1007   left:num <- get *editor, left:offset
1008   right:num <- get *editor, right:offset
1009   already-at-top?:bool <- lesser-or-equal cursor-row, 1/top
1010   {
1011     # if cursor not at top, move it
1012     break-if already-at-top?
1013     # if not at start of screen line, move to start of screen line (previous newline)
1014     # then scan back another line
1015     # if either step fails, give up without modifying cursor or coordinates
1016     curr:&:duplex-list:char <- copy before-cursor
1017     old:&:duplex-list:char <- copy curr
1018     {
1019       at-left?:bool <- equal cursor-column, left
1020       break-if at-left?
1021       curr <- before-previous-screen-line curr, editor
1022       no-motion?:bool <- equal curr, old
1023       return-if no-motion?
1024     }
1025     {
1026       curr <- before-previous-screen-line curr, editor
1027       no-motion?:bool <- equal curr, old
1028       return-if no-motion?
1029     }
1030     before-cursor <- copy curr
1031     *editor <- put *editor, before-cursor:offset, before-cursor
1032     cursor-row <- subtract cursor-row, 1
1033     *editor <- put *editor, cursor-row:offset, cursor-row
1034     # scan ahead to right column or until end of line
1035     target-column:num <- copy cursor-column
1036     cursor-column <- copy left
1037     *editor <- put *editor, cursor-column:offset, cursor-column
1038     {
1039       done?:bool <- greater-or-equal cursor-column, target-column
1040       break-if done?
1041       curr:&:duplex-list:char <- next before-cursor
1042       break-unless curr
1043       currc:char <- get *curr, value:offset
1044       at-newline?:bool <- equal currc, 10/newline
1045       break-if at-newline?
1046       #
1047       before-cursor <- copy curr
1048       *editor <- put *editor, before-cursor:offset, before-cursor
1049       cursor-column <- add cursor-column, 1
1050       *editor <- put *editor, cursor-column:offset, cursor-column
1051       loop
1052     }
1053     return
1054   }
1055   {
1056     # if cursor already at top, scroll up
1057     break-unless already-at-top?
1058     <scroll-up>
1059     return 1/go-render
1060   }
1061 ]
1062 
1063 scenario editor-adjusts-column-at-previous-line [
1064   local-scope
1065   assume-screen 10/width, 5/height
1066   s:text <- new [ab
1067 def]
1068   e:&:editor <- new-editor s, 0/left, 10/right
1069   editor-render screen, e
1070   $clear-trace
1071   assume-console [
1072     left-click 2, 3
1073     press up-arrow
1074   ]
1075   run [
1076     editor-event-loop screen, console, e
1077     3:num/raw <- get *e, cursor-row:offset
1078     4:num/raw <- get *e, cursor-column:offset
1079   ]
1080   memory-should-contain [
1081     3 <- 1
1082     4 <- 2
1083   ]
1084   check-trace-count-for-label 0, [print-character]
1085   assume-console [
1086     type [0]
1087   ]
1088   run [
1089     editor-event-loop screen, console, e
1090   ]
1091   screen-should-contain [
1092     .          .
1093     .ab0       .
1094     .def       .
1095     .╌╌╌╌╌╌╌╌╌╌.
1096     .          .
1097   ]
1098 ]
1099 
1100 scenario editor-adjusts-column-at-empty-line [
1101   local-scope
1102   assume-screen 10/width, 5/height
1103   s:text <- new [
1104 def]
1105   e:&:editor <- new-editor s, 0/left, 10/right
1106   editor-render screen, e
1107   $clear-trace
1108   assume-console [
1109     left-click 2, 3
1110     press up-arrow
1111   ]
1112   run [
1113     editor-event-loop screen, console, e
1114     3:num/raw <- get *e, cursor-row:offset
1115     4:num/raw <- get *e, cursor-column:offset
1116   ]
1117   memory-should-contain [
1118     3 <- 1
1119     4 <- 0
1120   ]
1121   check-trace-count-for-label 0, [print-character]
1122   assume-console [
1123     type [0]
1124   ]
1125   run [
1126     editor-event-loop screen, console, e
1127   ]
1128   screen-should-contain [
1129     .          .
1130     .0         .
1131     .def       .
1132     .╌╌╌╌╌╌╌╌╌╌.
1133     .          .
1134   ]
1135 ]
1136 
1137 scenario editor-moves-to-previous-line-from-zero-margin [
1138   local-scope
1139   assume-screen 10/width, 5/height
1140   # start out with three lines
1141   s:text <- new [abc
1142 def
1143 ghi]
1144   e:&:editor <- new-editor s, 0/left, 10/right
1145   editor-render screen, e
1146   $clear-trace
1147   # click on the third line and hit up-arrow, so you end up just after a newline
1148   assume-console [
1149     left-click 3, 0
1150     press up-arrow
1151   ]
1152   run [
1153     editor-event-loop screen, console, e
1154     3:num/raw <- get *e, cursor-row:offset
1155     4:num/raw <- get *e, cursor-column:offset
1156   ]
1157   memory-should-contain [
1158     3 <- 2
1159     4 <- 0
1160   ]
1161   check-trace-count-for-label 0, [print-character]
1162   assume-console [
1163     type [0]
1164   ]
1165   run [
1166     editor-event-loop screen, console, e
1167   ]
1168   screen-should-contain [
1169     .          .
1170     .abc       .
1171     .0def      .
1172     .ghi       .
1173     .╌╌╌╌╌╌╌╌╌╌.
1174   ]
1175 ]
1176 
1177 scenario editor-moves-to-previous-line-from-left-margin [
1178   local-scope
1179   assume-screen 10/width, 5/height
1180   # start out with three lines
1181   s:text <- new [abc
1182 def
1183 ghi]
1184   e:&:editor <- new-editor s, 1/left, 10/right
1185   editor-render screen, e
1186   $clear-trace
1187   # click on the third line and hit up-arrow, so you end up just after a newline
1188   assume-console [
1189     left-click 3, 1
1190     press up-arrow
1191   ]
1192   run [
1193     editor-event-loop screen, console, e
1194     3:num/raw <- get *e, cursor-row:offset
1195     4:num/raw <- get *e, cursor-column:offset
1196   ]
1197   memory-should-contain [
1198     3 <- 2
1199     4 <- 1
1200   ]
1201   check-trace-count-for-label 0, [print-character]
1202   assume-console [
1203     type [0]
1204   ]
1205   run [
1206     editor-event-loop screen, console, e
1207   ]
1208   screen-should-contain [
1209     .          .
1210     . abc      .
1211     . 0def     .
1212     . ghi      .
1213     . ╌╌╌╌╌╌╌╌╌.
1214   ]
1215 ]
1216 
1217 scenario editor-moves-to-top-line-in-presence-of-wrapped-line [
1218   local-scope
1219   assume-screen 10/width, 5/height
1220   e:&:editor <- new-editor [abcde], 0/left, 5/right
1221   editor-render screen, e
1222   screen-should-contain [
1223     .          .
1224     .abcd↩     .
1225     .e         .
1226     .╌╌╌╌╌     .
1227   ]
1228   $clear-trace
1229   assume-console [
1230     left-click 2, 0
1231     press up-arrow
1232   ]
1233   run [
1234     editor-event-loop screen, console, e
1235     3:num/raw <- get *e, cursor-row:offset
1236     4:num/raw <- get *e, cursor-column:offset
1237   ]
1238   memory-should-contain [
1239     3 <- 1
1240     4 <- 0
1241   ]
1242   check-trace-count-for-label 0, [print-character]
1243   assume-console [
1244     type [0]
1245   ]
1246   run [
1247     editor-event-loop screen, console, e
1248   ]
1249   screen-should-contain [
1250     .          .
1251     .0abc↩     .
1252     .de        .
1253     .╌╌╌╌╌     .
1254   ]
1255 ]
1256 
1257 scenario editor-moves-to-top-line-in-presence-of-wrapped-line-2 [
1258   local-scope
1259   assume-screen 10/width, 5/height
1260   s:text <- new [abc
1261 defgh]
1262   e:&:editor <- new-editor s, 0/left, 5/right
1263   editor-render screen, e
1264   screen-should-contain [
1265     .          .
1266     .abc       .
1267     .defg↩     .
1268     .h         .
1269     .╌╌╌╌╌     .
1270   ]
1271   $clear-trace
1272   assume-console [
1273     left-click 3, 0
1274     press up-arrow
1275     press up-arrow
1276   ]
1277   run [
1278     editor-event-loop screen, console, e
1279     3:num/raw <- get *e, cursor-row:offset
1280     4:num/raw <- get *e, cursor-column:offset
1281   ]
1282   memory-should-contain [
1283     3 <- 1
1284     4 <- 0
1285   ]
1286   check-trace-count-for-label 0, [print-character]
1287   assume-console [
1288     type [0]
1289   ]
1290   run [
1291     editor-event-loop screen, console, e
1292   ]
1293   screen-should-contain [
1294     .          .
1295     .0abc      .
1296     .defg↩     .
1297     .h         .
1298     .╌╌╌╌╌     .
1299   ]
1300 ]
1301 
1302 # down arrow
1303 
1304 scenario editor-moves-to-next-line-with-down-arrow [
1305   local-scope
1306   assume-screen 10/width, 5/height
1307   s:text <- new [abc
1308 def]
1309   e:&:editor <- new-editor s, 0/left, 10/right
1310   editor-render screen, e
1311   $clear-trace
1312   # cursor starts out at (1, 0)
1313   assume-console [
1314     press down-arrow
1315   ]
1316   run [
1317     editor-event-loop screen, console, e
1318     3:num/raw <- get *e, cursor-row:offset
1319     4:num/raw <- get *e, cursor-column:offset
1320   ]
1321   # ..and ends at (2, 0)
1322   memory-should-contain [
1323     3 <- 2
1324     4 <- 0
1325   ]
1326   check-trace-count-for-label 0, [print-character]
1327   assume-console [
1328     type [0]
1329   ]
1330   run [
1331     editor-event-loop screen, console, e
1332   ]
1333   screen-should-contain [
1334     .          .
1335     .abc       .
1336     .0def      .
1337     .╌╌╌╌╌╌╌╌╌╌.
1338     .          .
1339   ]
1340 ]
1341 
1342 after <handle-special-key> [
1343   {
1344     move-to-next-line?:bool <- equal k, 65516/down-arrow
1345     break-unless move-to-next-line?
1346     <begin-move-cursor>
1347     go-render? <- move-to-next-line editor, screen-height
1348     undo-coalesce-tag:num <- copy 4/down-arrow
1349     <end-move-cursor>
1350     return
1351   }
1352 ]
1353 
1354 def move-to-next-line editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
1355   local-scope
1356   load-inputs
1357   cursor-row:num <- get *editor, cursor-row:offset
1358   cursor-column:num <- get *editor, cursor-column:offset
1359   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1360   left:num <- get *editor, left:offset
1361   right:num <- get *editor, right:offset
1362   last-line:num <- subtract screen-height, 1
1363   bottom:num <- get *editor, bottom:offset
1364   at-bottom-of-screen?:bool <- greater-or-equal bottom, last-line
1365   {
1366     break-if before-cursor
1367     {
1368       break-if at-bottom-of-screen?
1369       return 0/don't-render
1370     }
1371     {
1372       break-unless at-bottom-of-screen?
1373       jump +try-to-scroll
1374     }
1375   }
1376   next:&:duplex-list:char <- next before-cursor
1377   {
1378     break-if next
1379     {
1380       break-if at-bottom-of-screen?
1381       return 0/don't-render
1382     }
1383     {
1384       break-unless at-bottom-of-screen?
1385       jump +try-to-scroll
1386     }
1387   }
1388   already-at-bottom?:bool <- greater-or-equal cursor-row, last-line
1389   {
1390     # if cursor not at bottom, move it
1391     break-if already-at-bottom?
1392     target-column:num <- copy cursor-column
1393     # scan to start of next line
1394     {
1395       next:&:duplex-list:char <- next before-cursor
1396       break-unless next
1397       done?:bool <- greater-or-equal cursor-column, right
1398       break-if done?
1399       cursor-column <- add cursor-column, 1
1400       before-cursor <- copy next
1401       c:char <- get *next, value:offset
1402       at-newline?:bool <- equal c, 10/newline
1403       break-if at-newline?
1404       loop
1405     }
1406     {
1407       break-if next
1408       {
1409         break-if at-bottom-of-screen?
1410         return 0/don't-render
1411       }
1412       {
1413         break-unless at-bottom-of-screen?
1414         jump +try-to-scroll
1415       }
1416     }
1417     cursor-row <- add cursor-row, 1
1418     cursor-column <- copy left
1419     {
1420       next:&:duplex-list:char <- next before-cursor
1421       break-unless next
1422       c:char <- get *next, value:offset
1423       at-newline?:bool <- equal c, 10/newline
1424       break-if at-newline?
1425       done?:bool <- greater-or-equal cursor-column, target-column
1426       break-if done?
1427       cursor-column <- add cursor-column, 1
1428       before-cursor <- copy next
1429       loop
1430     }
1431     *editor <- put *editor, before-cursor:offset, before-cursor
1432     *editor <- put *editor, cursor-column:offset, cursor-column
1433     *editor <- put *editor, cursor-row:offset, cursor-row
1434     return 0/don't-render
1435   }
1436   +try-to-scroll
1437   <scroll-down>
1438   go-render? <- copy 1/true
1439 ]
1440 
1441 scenario editor-adjusts-column-at-next-line [
1442   local-scope
1443   assume-screen 10/width, 5/height
1444   # second line is shorter than first
1445   s:text <- new [abcde
1446 fg
1447 hi]
1448   e:&:editor <- new-editor s, 0/left, 10/right
1449   editor-render screen, e
1450   $clear-trace
1451   # move to end of first line, then press down
1452   assume-console [
1453     left-click 1, 8
1454     press down-arrow
1455   ]
1456   run [
1457     editor-event-loop screen, console, e
1458     3:num/raw <- get *e, cursor-row:offset
1459     4:num/raw <- get *e, cursor-column:offset
1460   ]
1461   # cursor doesn't go vertically down, it goes to end of shorter line
1462   memory-should-contain [
1463     3 <- 2
1464     4 <- 2
1465   ]
1466   check-trace-count-for-label 0, [print-character]
1467   assume-console [
1468     type [0]
1469   ]
1470   run [
1471     editor-event-loop screen, console, e
1472   ]
1473   screen-should-contain [
1474     .          .
1475     .abcde     .
1476     .fg0       .
1477     .hi        .
1478     .╌╌╌╌╌╌╌╌╌╌.
1479   ]
1480 ]
1481 
1482 scenario editor-moves-down-within-wrapped-line [
1483   local-scope
1484   assume-screen 10/width, 5/height
1485   e:&:editor <- new-editor [abcdefghijklmno], 0/left, 10/right
1486   editor-render screen, e
1487   screen-should-contain [
1488     .          .
1489     .abcdefghi↩.
1490     .jklmno    .
1491     .╌╌╌╌╌╌╌╌╌╌.
1492     .          .
1493   ]
1494   # position cursor on first screen line, but past end of second screen line
1495   assume-console [
1496     left-click 1, 8
1497     press down-arrow
1498   ]
1499   run [
1500     editor-event-loop screen, console, e
1501     3:num/raw <- get *e, cursor-row:offset
1502     4:num/raw <- get *e, cursor-column:offset
1503   ]
1504   # cursor should be at end of second screen line
1505   memory-should-contain [
1506     3 <- 2
1507     4 <- 6
1508   ]
1509 ]
1510 
1511 # ctrl-a/home - move cursor to start of line
1512 
1513 scenario editor-moves-to-start-of-line-with-ctrl-a [
1514   local-scope
1515   assume-screen 10/width, 5/height
1516   s:text <- new [123
1517 456]
1518   e:&:editor <- new-editor s, 0/left, 10/right
1519   editor-render screen, e
1520   $clear-trace
1521   # start on second line, press ctrl-a
1522   assume-console [
1523     left-click 2, 3
1524     press ctrl-a
1525   ]
1526   run [
1527     editor-event-loop screen, console, e
1528     4:num/raw <- get *e, cursor-row:offset
1529     5:num/raw <- get *e, cursor-column:offset
1530   ]
1531   # cursor moves to start of line
1532   memory-should-contain [
1533     4 <- 2
1534     5 <- 0
1535   ]
1536   check-trace-count-for-label 0, [print-character]
1537 ]
1538 
1539 after <handle-special-character> [
1540   {
1541     move-to-start-of-line?:bool <- equal c, 1/ctrl-a
1542     break-unless move-to-start-of-line?
1543     <begin-move-cursor>
1544     move-to-start-of-screen-line editor
1545     undo-coalesce-tag:num <- copy 0/never
1546     <end-move-cursor>
1547     return 0/don't-render
1548   }
1549 ]
1550 
1551 after <handle-special-key> [
1552   {
1553     move-to-start-of-line?:bool <- equal k, 65521/home
1554     break-unless move-to-start-of-line?
1555     <begin-move-cursor>
1556     move-to-start-of-screen-line editor
1557     undo-coalesce-tag:num <- copy 0/never
1558     <end-move-cursor>
1559     return 0/don't-render
1560   }
1561 ]
1562 
1563 # handles wrapped lines
1564 # precondition: cursor-column should be in a consistent state
1565 def move-to-start-of-screen-line editor:&:editor -> editor:&:editor [
1566   local-scope
1567   load-inputs
1568   # update cursor column
1569   left:num <- get *editor, left:offset
1570   col:num <- get *editor, cursor-column:offset
1571   # update before-cursor
1572   curr:&:duplex-list:char <- get *editor, before-cursor:offset
1573   # while not at start of line, move
1574   {
1575     done?:bool <- equal col, left
1576     break-if done?
1577     assert curr, [move-to-start-of-line tried to move before start of text]
1578     curr <- prev curr
1579     col <- subtract col, 1
1580     loop
1581   }
1582   *editor <- put *editor, cursor-column:offset, col
1583   *editor <- put *editor, before-cursor:offset, curr
1584 ]
1585 
1586 scenario editor-moves-to-start-of-line-with-ctrl-a-2 [
1587   local-scope
1588   assume-screen 10/width, 5/height
1589   s:text <- new [123
1590 456]
1591   e:&:editor <- new-editor s, 0/left, 10/right
1592   editor-render screen, e
1593   $clear-trace
1594   # start on first line (no newline before), press ctrl-a
1595   assume-console [
1596     left-click 1, 3
1597     press ctrl-a
1598   ]
1599   run [
1600     editor-event-loop screen, console, e
1601     4:num/raw <- get *e, cursor-row:offset
1602     5:num/raw <- get *e, cursor-column:offset
1603   ]
1604   # cursor moves to start of line
1605   memory-should-contain [
1606     4 <- 1
1607     5 <- 0
1608   ]
1609   check-trace-count-for-label 0, [print-character]
1610 ]
1611 
1612 scenario editor-moves-to-start-of-line-with-home [
1613   local-scope
1614   assume-screen 10/width, 5/height
1615   s:text <- new [123
1616 456]
1617   e:&:editor <- new-editor s, 0/left, 10/right
1618   $clear-trace
1619   # start on second line, press 'home'
1620   assume-console [
1621     left-click 2, 3
1622     press home
1623   ]
1624   run [
1625     editor-event-loop screen, console, e
1626     3:num/raw <- get *e, cursor-row:offset
1627     4:num/raw <- get *e, cursor-column:offset
1628   ]
1629   # cursor moves to start of line
1630   memory-should-contain [
1631     3 <- 2
1632     4 <- 0
1633   ]
1634   check-trace-count-for-label 0, [print-character]
1635 ]
1636 
1637 scenario editor-moves-to-start-of-line-with-home-2 [
1638   local-scope
1639   assume-screen 10/width, 5/height
1640   s:text <- new [123
1641 456]
1642   e:&:editor <- new-editor s, 0/left, 10/right
1643   editor-render screen, e
1644   $clear-trace
1645   # start on first line (no newline before), press 'home'
1646   assume-console [
1647     left-click 1, 3
1648     press home
1649   ]
1650   run [
1651     editor-event-loop screen, console, e
1652     3:num/raw <- get *e, cursor-row:offset
1653     4:num/raw <- get *e, cursor-column:offset
1654   ]
1655   # cursor moves to start of line
1656   memory-should-contain [
1657     3 <- 1
1658     4 <- 0
1659   ]
1660   check-trace-count-for-label 0, [print-character]
1661 ]
1662 
1663 scenario editor-moves-to-start-of-screen-line-with-ctrl-a [
1664   local-scope
1665   assume-screen 10/width, 5/height
1666   e:&:editor <- new-editor [123456], 0/left, 5/right
1667   editor-render screen, e
1668   screen-should-contain [
1669     .          .
1670     .1234↩     .
1671     .56        .
1672     .╌╌╌╌╌     .
1673     .          .
1674   ]
1675   $clear-trace
1676   # start on second line, press ctrl-a then up
1677   assume-console [
1678     left-click 2, 1
1679     press ctrl-a
1680     press up-arrow
1681   ]
1682   run [
1683     editor-event-loop screen, console, e
1684     4:num/raw <- get *e, cursor-row:offset
1685     5:num/raw <- get *e, cursor-column:offset
1686   ]
1687   # cursor moves to start of first line
1688   memory-should-contain [
1689     4 <- 1  # cursor-row
1690     5 <- 0  # cursor-column
1691   ]
1692   check-trace-count-for-label 0, [print-character]
1693   # make sure before-cursor is in sync
1694   assume-console [
1695     type [a]
1696   ]
1697   run [
1698     editor-event-loop screen, console, e
1699     4:num/raw <- get *e, cursor-row:offset
1700     5:num/raw <- get *e, cursor-column:offset
1701   ]
1702   screen-should-contain [
1703     .          .
1704     .a123↩     .
1705     .456       .
1706     .╌╌╌╌╌     .
1707     .          .
1708   ]
1709   memory-should-contain [
1710     4 <- 1  # cursor-row
1711     5 <- 1  # cursor-column
1712   ]
1713 ]
1714 
1715 # ctrl-e/end - move cursor to end of line
1716 
1717 scenario editor-moves-to-end-of-line-with-ctrl-e [
1718   local-scope
1719   assume-screen 10/width, 5/height
1720   s:text <- new [123
1721 456]
1722   e:&:editor <- new-editor s, 0/left, 10/right
1723   editor-render screen, e
1724   $clear-trace
1725   # start on first line, press ctrl-e
1726   assume-console [
1727     left-click 1, 1
1728     press ctrl-e
1729   ]
1730   run [
1731     editor-event-loop screen, console, e
1732     4:num/raw <- get *e, cursor-row:offset
1733     5:num/raw <- get *e, cursor-column:offset
1734   ]
1735   # cursor moves to end of line
1736   memory-should-contain [
1737     4 <- 1
1738     5 <- 3
1739   ]
1740   check-trace-count-for-label 0, [print-character]
1741   # editor inserts future characters at cursor
1742   assume-console [
1743     type [z]
1744   ]
1745   run [
1746     editor-event-loop screen, console, e
1747     4:num/raw <- get *e, cursor-row:offset
1748     5:num/raw <- get *e, cursor-column:offset
1749   ]
1750   memory-should-contain [
1751     4 <- 1
1752     5 <- 4
1753   ]
1754   screen-should-contain [
1755     .          .
1756     .123z      .
1757     .456       .
1758     .╌╌╌╌╌╌╌╌╌╌.
1759     .          .
1760   ]
1761   check-trace-count-for-label 1, [print-character]
1762 ]
1763 
1764 after <handle-special-character> [
1765   {
1766     move-to-end-of-line?:bool <- equal c, 5/ctrl-e
1767     break-unless move-to-end-of-line?
1768     <begin-move-cursor>
1769     move-to-end-of-line editor
1770     undo-coalesce-tag:num <- copy 0/never
1771     <end-move-cursor>
1772     return 0/don't-render
1773   }
1774 ]
1775 
1776 after <handle-special-key> [
1777   {
1778     move-to-end-of-line?:bool <- equal k, 65520/end
1779     break-unless move-to-end-of-line?
1780     <begin-move-cursor>
1781     move-to-end-of-line editor
1782     undo-coalesce-tag:num <- copy 0/never
1783     <end-move-cursor>
1784     return 0/don't-render
1785   }
1786 ]
1787 
1788 def move-to-end-of-line editor:&:editor -> editor:&:editor [
1789   local-scope
1790   load-inputs
1791   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1792   cursor-column:num <- get *editor, cursor-column:offset
1793   right:num <- get *editor, right:offset
1794   # while not at end of line, move
1795   {
1796     next:&:duplex-list:char <- next before-cursor
1797     break-unless next  # end of text
1798     nextc:char <- get *next, value:offset
1799     at-end-of-line?:bool <- equal nextc, 10/newline
1800     break-if at-end-of-line?
1801     cursor-column <- add cursor-column, 1
1802     at-right?:bool <- equal cursor-column, right
1803     break-if at-right?
1804     *editor <- put *editor, cursor-column:offset, cursor-column
1805     before-cursor <- copy next
1806     *editor <- put *editor, before-cursor:offset, before-cursor
1807     loop
1808   }
1809 ]
1810 
1811 scenario editor-moves-to-end-of-line-with-ctrl-e-2 [
1812   local-scope
1813   assume-screen 10/width, 5/height
1814   s:text <- new [123
1815 456]
1816   e:&:editor <- new-editor s, 0/left, 10/right
1817   editor-render screen, e
1818   $clear-trace
1819   # start on second line (no newline after), press ctrl-e
1820   assume-console [
1821     left-click 2, 1
1822     press ctrl-e
1823   ]
1824   run [
1825     editor-event-loop screen, console, e
1826     4:num/raw <- get *e, cursor-row:offset
1827     5:num/raw <- get *e, cursor-column:offset
1828   ]
1829   # cursor moves to end of line
1830   memory-should-contain [
1831     4 <- 2
1832     5 <- 3
1833   ]
1834   check-trace-count-for-label 0, [print-character]
1835 ]
1836 
1837 scenario editor-moves-to-end-of-line-with-end [
1838   local-scope
1839   assume-screen 10/width, 5/height
1840   s:text <- new [123
1841 456]
1842   e:&:editor <- new-editor s, 0/left, 10/right
1843   editor-render screen, e
1844   $clear-trace
1845   # start on first line, press 'end'
1846   assume-console [
1847     left-click 1, 1
1848     press end
1849   ]
1850   run [
1851     editor-event-loop screen, console, e
1852     3:num/raw <- get *e, cursor-row:offset
1853     4:num/raw <- get *e, cursor-column:offset
1854   ]
1855   # cursor moves to end of line
1856   memory-should-contain [
1857     3 <- 1
1858     4 <- 3
1859   ]
1860   check-trace-count-for-label 0, [print-character]
1861 ]
1862 
1863 scenario editor-moves-to-end-of-line-with-end-2 [
1864   local-scope
1865   assume-screen 10/width, 5/height
1866   s:text <- new [123
1867 456]
1868   e:&:editor <- new-editor s, 0/left, 10/right
1869   editor-render screen, e
1870   $clear-trace
1871   # start on second line (no newline after), press 'end'
1872   assume-console [
1873     left-click 2, 1
1874     press end
1875   ]
1876   run [
1877     editor-event-loop screen, console, e
1878     3:num/raw <- get *e, cursor-row:offset
1879     4:num/raw <- get *e, cursor-column:offset
1880   ]
1881   # cursor moves to end of line
1882   memory-should-contain [
1883     3 <- 2
1884     4 <- 3
1885   ]
1886   check-trace-count-for-label 0, [print-character]
1887 ]
1888 
1889 scenario editor-moves-to-end-of-wrapped-line [
1890   local-scope
1891   assume-screen 10/width, 5/height
1892   s:text <- new [123456
1893 789]
1894   e:&:editor <- new-editor s, 0/left, 5/right
1895   editor-render screen, e
1896   $clear-trace
1897   # start on first line, press 'end'
1898   assume-console [
1899     left-click 1, 1
1900     press end
1901   ]
1902   run [
1903     editor-event-loop screen, console, e
1904     10:num/raw <- get *e, cursor-row:offset
1905     11:num/raw <- get *e, cursor-column:offset
1906   ]
1907   # cursor moves to end of line
1908   memory-should-contain [
1909     10 <- 1
1910     11 <- 3
1911   ]
1912   # no prints
1913   check-trace-count-for-label 0, [print-character]
1914   # before-cursor is also consistent
1915   assume-console [
1916     type [a]
1917   ]
1918   run [
1919     editor-event-loop screen, console, e
1920   ]
1921   screen-should-contain [
1922     .          .
1923     .123a↩     .
1924     .456       .
1925     .789       .
1926     .╌╌╌╌╌     .
1927   ]
1928 ]
1929 
1930 # ctrl-u - delete text from start of line until (but not at) cursor
1931 
1932 scenario editor-deletes-to-start-of-line-with-ctrl-u [
1933   local-scope
1934   assume-screen 10/width, 5/height
1935   s:text <- new [123
1936 456]
1937   e:&:editor <- new-editor s, 0/left, 10/right
1938   editor-render screen, e
1939   $clear-trace
1940   # start on second line, press ctrl-u
1941   assume-console [
1942     left-click 2, 2
1943     press ctrl-u
1944   ]
1945   run [
1946     editor-event-loop screen, console, e
1947   ]
1948   # cursor deletes to start of line
1949   screen-should-contain [
1950     .          .
1951     .123       .
1952     .6         .
1953     .╌╌╌╌╌╌╌╌╌╌.
1954     .          .
1955   ]
1956   check-trace-count-for-label 10, [print-character]
1957 ]
1958 
1959 after <handle-special-character> [
1960   {
1961     delete-to-start-of-line?:bool <- equal c, 21/ctrl-u
1962     break-unless delete-to-start-of-line?
1963     <begin-delete-to-start-of-line>
1964     deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor
1965     <end-delete-to-start-of-line>
1966     go-render?:bool <- minimal-render-for-ctrl-u screen, editor, deleted-cells
1967     return
1968   }
1969 ]
1970 
1971 def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
1972   local-scope
1973   load-inputs
1974   curr-column:num <- get *editor, cursor-column:offset
1975   # accumulate the current line as text and render it
1976   buf:&:buffer:char <- new-buffer 30  # accumulator for the text we need to render
1977   curr:&:duplex-list:char <- get *editor, before-cursor:offset
1978   i:num <- copy curr-column
1979   right:num <- get *editor, right:offset
1980   {
1981     # if we have a wrapped line, give up and render the whole screen
1982     wrap?:bool <- greater-or-equal i, right
1983     return-if wrap?, 1/go-render
1984     curr <- next curr
1985     break-unless curr
1986     c:char <- get *curr, value:offset
1987     b:bool <- equal c, 10
1988     break-if b
1989     buf <- append buf, c
1990     i <- add i, 1
1991     loop
1992   }
1993   # if the line used to be wrapped, give up and render the whole screen
1994   num-deleted-cells:num <- length deleted-cells
1995   old-row-len:num <- add i, num-deleted-cells
1996   left:num <- get *editor, left:offset
1997   end:num <- subtract right, left
1998   wrap?:bool <- greater-or-equal old-row-len, end
1999   return-if wrap?, 1/go-render
2000   curr-line:text <- buffer-to-array buf
2001   curr-row:num <- get *editor, cursor-row:offset
2002   render-code screen, curr-line, curr-column, right, curr-row
2003   return 0/dont-render
2004 ]
2005 
2006 def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
2007   local-scope
2008   load-inputs
2009   # compute range to delete
2010   init:&:duplex-list:char <- get *editor, data:offset
2011   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2012   update-top-of-screen?:bool <- copy 0/false
2013   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
2014   start:&:duplex-list:char <- copy before-cursor
2015   end:&:duplex-list:char <- next before-cursor
2016   {
2017     at-start-of-text?:bool <- equal start, init
2018     break-if at-start-of-text?
2019     curr:char <- get *start, value:offset
2020     at-start-of-line?:bool <- equal curr, 10/newline
2021     break-if at-start-of-line?
2022     # if we went past top-of-screen, make a note to update it as well
2023     at-top-of-screen?:bool <- equal start, top-of-screen
2024     update-top-of-screen?:bool <- or update-top-of-screen?, at-top-of-screen?
2025     start <- prev start
2026     assert start, [delete-to-start-of-line tried to move before start of text]
2027     loop
2028   }
2029   # snip it out
2030   result:&:duplex-list:char <- next start
2031   remove-between start, end
2032   # update top-of-screen if it's just been invalidated
2033   {
2034     break-unless update-top-of-screen?
2035     put *editor, top-of-screen:offset, start
2036   }
2037   # adjust cursor
2038   before-cursor <- copy start
2039   *editor <- put *editor, before-cursor:offset, before-cursor
2040   left:num <- get *editor, left:offset
2041   *editor <- put *editor, cursor-column:offset, left
2042   # if the line wrapped before, we may need to adjust cursor-row as well
2043   right:num <- get *editor, right:offset
2044   width:num <- subtract right, left
2045   num-deleted:num <- length result
2046   cursor-row-adjustment:num <- divide-with-remainder num-deleted, width
2047   return-unless cursor-row-adjustment
2048   cursor-row:num <- get *editor, cursor-row:offset
2049   cursor-row-in-editor:num <- subtract cursor-row, 1  # ignore menubar
2050   at-top?:bool <- lesser-or-equal cursor-row-in-editor, cursor-row-adjustment
2051   {
2052     break-unless at-top?
2053     cursor-row <- copy 1  # top of editor, below menubar
2054   }
2055   {
2056     break-if at-top?
2057     cursor-row <- subtract cursor-row, cursor-row-adjustment
2058   }
2059   put *editor, cursor-row:offset, cursor-row
2060 ]
2061 
2062 def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [
2063   local-scope
2064   load-inputs
2065   return-unless s
2066   color:num <- copy 7/white
2067   column:num <- copy left
2068   screen <- move-cursor screen, row, column
2069   screen-height:num <- screen-height screen
2070   i:num <- copy 0
2071   len:num <- length *s
2072   {
2073     +next-character
2074     done?:bool <- greater-or-equal i, len
2075     break-if done?
2076     done? <- greater-or-equal row, screen-height
2077     break-if done?
2078     c:char <- index *s, i
2079     <character-c-received>
2080     {
2081       # newline? move to left rather than 0
2082       newline?:bool <- equal c, 10/newline
2083       break-unless newline?
2084       # clear rest of line in this window
2085       {
2086         done?:bool <- greater-than column, right
2087         break-if done?
2088         space:char <- copy 32/space
2089         print screen, space
2090         column <- add column, 1
2091         loop
2092       }
2093       row <- add row, 1
2094       column <- copy left
2095       screen <- move-cursor screen, row, column
2096       i <- add i, 1
2097       loop +next-character
2098     }
2099     {
2100       # at right? wrap.
2101       at-right?:bool <- equal column, right
2102       break-unless at-right?
2103       # print wrap icon
2104       wrap-icon:char <- copy 8617/loop-back-to-left
2105       print screen, wrap-icon, 245/grey
2106       column <- copy left
2107       row <- add row, 1
2108       screen <- move-cursor screen, row, column
2109       # don't increment i
2110       loop +next-character
2111     }
2112     i <- add i, 1
2113     print screen, c, color
2114     column <- add column, 1
2115     loop
2116   }
2117   was-at-left?:bool <- equal column, left
2118   clear-line-until screen, right
2119   {
2120     break-if was-at-left?
2121     row <- add row, 1
2122   }
2123   move-cursor screen, row, left
2124 ]
2125 
2126 scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
2127   local-scope
2128   assume-screen 10/width, 5/height
2129   s:text <- new [123
2130 456]
2131   e:&:editor <- new-editor s, 0/left, 10/right
2132   editor-render screen, e
2133   $clear-trace
2134   # start on first line (no newline before), press ctrl-u
2135   assume-console [
2136     left-click 1, 2
2137     press ctrl-u
2138   ]
2139   run [
2140     editor-event-loop screen, console, e
2141   ]
2142   # cursor deletes to start of line
2143   screen-should-contain [
2144     .          .
2145     .3         .
2146     .456       .
2147     .╌╌╌╌╌╌╌╌╌╌.
2148     .          .
2149   ]
2150   check-trace-count-for-label 10, [print-character]
2151 ]
2152 
2153 scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
2154   local-scope
2155   assume-screen 10/width, 5/height
2156   s:text <- new [123
2157 456]
2158   e:&:editor <- new-editor s, 0/left, 10/right
2159   editor-render screen, e
2160   $clear-trace
2161   # start past end of line, press ctrl-u
2162   assume-console [
2163     left-click 1, 3
2164     press ctrl-u
2165   ]
2166   run [
2167     editor-event-loop screen, console, e
2168   ]
2169   # cursor deletes to start of line
2170   screen-should-contain [
2171     .          .
2172     .          .
2173     .456       .
2174     .╌╌╌╌╌╌╌╌╌╌.
2175     .          .
2176   ]
2177   check-trace-count-for-label 10, [print-character]
2178 ]
2179 
2180 scenario editor-deletes-to-start-of-final-line-with-ctrl-u [
2181   local-scope
2182   assume-screen 10/width, 5/height
2183   s:text <- new [123
2184 456]
2185   e:&:editor <- new-editor s, 0/left, 10/right
2186   editor-render screen, e
2187   $clear-trace
2188   # start past end of final line, press ctrl-u
2189   assume-console [
2190     left-click 2, 3
2191     press ctrl-u
2192   ]
2193   run [
2194     editor-event-loop screen, console, e
2195   ]
2196   # cursor deletes to start of line
2197   screen-should-contain [
2198     .          .
2199     .123       .
2200     .          .
2201     .╌╌╌╌╌╌╌╌╌╌.
2202     .          .
2203   ]
2204   check-trace-count-for-label 10, [print-character]
2205 ]
2206 
2207 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u [
2208   local-scope
2209   assume-screen 10/width, 10/height
2210   # first line starts out wrapping
2211   s:text <- new [123456
2212 789]
2213   e:&:editor <- new-editor s, 0/left, 5/right
2214   editor-render screen, e
2215   screen-should-contain [
2216     .          .
2217     .1234↩     .
2218     .56        .
2219     .789       .
2220     .╌╌╌╌╌     .
2221     .          .
2222   ]
2223   $clear-trace
2224   # ctrl-u enough of the first line that it's no longer wrapping
2225   assume-console [
2226     left-click 1, 3
2227     press ctrl-u
2228   ]
2229   run [
2230     editor-event-loop screen, console, e
2231   ]
2232   # entire screen needs to be refreshed
2233   screen-should-contain [
2234     .          .
2235     .456       .
2236     .789       .
2237     .╌╌╌╌╌     .
2238     .          .
2239   ]
2240   check-trace-count-for-label 45, [print-character]
2241 ]
2242 
2243 # sometimes hitting ctrl-u needs to adjust the cursor row
2244 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-2 [
2245   local-scope
2246   assume-screen 10/width, 10/height
2247   # third line starts out wrapping
2248   s:text <- new [1
2249 2
2250 345678
2251 9]
2252   e:&:editor <- new-editor s, 0/left, 5/right
2253   editor-render screen, e
2254   screen-should-contain [
2255     .          .
2256     .1         .
2257     .2         .
2258     .3456↩     .
2259     .78        .
2260     .9         .
2261     .╌╌╌╌╌     .
2262     .          .
2263   ]
2264   # position cursor on screen line after the wrap and hit ctrl-u
2265   assume-console [
2266     left-click 4, 1  # on '8'
2267     press ctrl-u
2268   ]
2269   run [
2270     editor-event-loop screen, console, e
2271     10:num/raw <- get *e, cursor-row:offset
2272     11:num/raw <- get *e, cursor-column:offset
2273   ]
2274   screen-should-contain [
2275     .          .
2276     .1         .
2277     .2         .
2278     .8         .
2279     .9         .
2280     .╌╌╌╌╌     .
2281     .          .
2282   ]
2283   # cursor moves up one screen line
2284   memory-should-contain [
2285     10 <- 3  # cursor-row
2286     11 <- 0  # cursor-column
2287   ]
2288 ]
2289 
2290 # line wrapping twice (taking up 3 screen lines)
2291 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-3 [
2292   local-scope
2293   assume-screen 10/width, 10/height
2294   # third line starts out wrapping
2295   s:text <- new [1
2296 2
2297 3456789abcd
2298 e]
2299   e:&:editor <- new-editor s, 0/left, 5/right
2300   editor-render screen, e
2301   assume-console [
2302     left-click 4, 1  # on '8'
2303   ]
2304   editor-event-loop screen, console, e
2305   screen-should-contain [
2306     .          .
2307     .1         .
2308     .2         .
2309     .3456↩     .
2310     .789a↩     .
2311     .bcd       .
2312     .e         .
2313     .╌╌╌╌╌     .
2314     .          .
2315   ]
2316   assume-console [
2317     left-click 5, 1
2318     press ctrl-u
2319   ]
2320   run [
2321     editor-event-loop screen, console, e
2322     10:num/raw <- get *e, cursor-row:offset
2323     11:num/raw <- get *e, cursor-column:offset
2324   ]
2325   screen-should-contain [
2326     .          .
2327     .1         .
2328     .2         .
2329     .cd        .
2330     .e         .
2331     .╌╌╌╌╌     .
2332     .          .
2333   ]
2334   # make sure we adjusted cursor-row
2335   memory-should-contain [
2336     10 <- 3  # cursor-row
2337     11 <- 0  # cursor-column
2338   ]
2339 ]
2340 
2341 # adjusting cursor row at the top of the screen
2342 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-4 [
2343   local-scope
2344   assume-screen 10/width, 10/height
2345   # first line starts out wrapping
2346   s:text <- new [1234567
2347 89]
2348   e:&:editor <- new-editor s, 0/left, 5/right
2349   editor-render screen, e
2350   screen-should-contain [
2351     .          .
2352     .1234↩     .
2353     .567       .
2354     .89        .
2355     .╌╌╌╌╌     .
2356     .          .
2357   ]
2358   # position cursor on second screen line (after the wrap) and hit ctrl-u
2359   assume-console [
2360     left-click 2, 1
2361     press ctrl-u
2362   ]
2363   run [
2364     editor-event-loop screen, console, e
2365     10:num/raw <- get *e, cursor-row:offset
2366     11:num/raw <- get *e, cursor-column:offset
2367   ]
2368   screen-should-contain [
2369     .          .
2370     .67        .
2371     .89        .
2372     .╌╌╌╌╌     .
2373     .          .
2374   ]
2375   # cursor moves up to screen line 1
2376   memory-should-contain [
2377     10 <- 1  # cursor-row
2378     11 <- 0  # cursor-column
2379   ]
2380 ]
2381 
2382 # screen begins part-way through a wrapping line
2383 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-5 [
2384   local-scope
2385   assume-screen 10/width, 10/height
2386   # third line starts out wrapping
2387   s:text <- new [1
2388 2
2389 345678
2390 9]
2391   e:&:editor <- new-editor s, 0/left, 5/right
2392   editor-render screen, e
2393   # position the '78' line at the top of the screen
2394   assume-console [
2395     left-click 4, 1  # on '8'
2396     press ctrl-t
2397   ]
2398   editor-event-loop screen, console, e
2399   screen-should-contain [
2400     .          .
2401     .78        .
2402     .9         .
2403     .╌╌╌╌╌     .
2404     .          .
2405   ]
2406   assume-console [
2407     left-click 1, 1
2408     press ctrl-u
2409   ]
2410   run [
2411     editor-event-loop screen, console, e
2412     10:num/raw <- get *e, cursor-row:offset
2413     11:num/raw <- get *e, cursor-column:offset
2414   ]
2415   # make sure we updated top-of-screen correctly
2416   screen-should-contain [
2417     .          .
2418     .8         .
2419     .9         .
2420     .╌╌╌╌╌     .
2421     .          .
2422   ]
2423   memory-should-contain [
2424     10 <- 1  # cursor-row
2425     11 <- 0  # cursor-column
2426   ]
2427   # the entire line is deleted, even the part not shown on screen
2428   assume-console [
2429     press up-arrow
2430   ]
2431   run [
2432     editor-event-loop screen, console, e
2433   ]
2434   screen-should-contain [
2435     .          .
2436     .2         .
2437     .8         .
2438     .9         .
2439     .╌╌╌╌╌     .
2440     .          .
2441   ]
2442 ]
2443 
2444 # screen begins part-way through a line wrapping twice (taking up 3 screen lines)
2445 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-6 [
2446   local-scope
2447   assume-screen 10/width, 10/height
2448   # third line starts out wrapping
2449   s:text <- new [1
2450 2
2451 3456789abcd
2452 e]
2453   e:&:editor <- new-editor s, 0/left, 5/right
2454   editor-render screen, e
2455   # position the 'bcd' line at the top of the screen
2456   assume-console [
2457     left-click 4, 1  # on '8'
2458     press ctrl-t
2459     press ctrl-s  # now on 'c'
2460   ]
2461   editor-event-loop screen, console, e
2462   screen-should-contain [
2463     .          .
2464     .bcd       .
2465     .e         .
2466     .╌╌╌╌╌     .
2467     .          .
2468   ]
2469   assume-console [
2470     left-click 1, 1
2471     press ctrl-u
2472   ]
2473   run [
2474     editor-event-loop screen, console, e
2475     10:num/raw <- get *e, cursor-row:offset
2476     11:num/raw <- get *e, cursor-column:offset
2477   ]
2478   # make sure we updated top-of-screen correctly
2479   screen-should-contain [
2480     .          .
2481     .cd        .
2482     .e         .
2483     .╌╌╌╌╌     .
2484     .          .
2485   ]
2486   memory-should-contain [
2487     10 <- 1  # cursor-row
2488     11 <- 0  # cursor-column
2489   ]
2490   # the entire line is deleted, even the part not shown on screen
2491   assume-console [
2492     press up-arrow
2493   ]
2494   run [
2495     editor-event-loop screen, console, e
2496   ]
2497   screen-should-contain [
2498     .          .
2499     .2         .
2500     .cd        .
2501     .e         .
2502     .╌╌╌╌╌     .
2503     .          .
2504   ]
2505 ]
2506 
2507 # ctrl-k - delete text from cursor to end of line (but not the newline)
2508 
2509 scenario editor-deletes-to-end-of-line-with-ctrl-k [
2510   local-scope
2511   assume-screen 10/width, 5/height
2512   s:text <- new [123
2513 456]
2514   e:&:editor <- new-editor s, 0/left, 10/right
2515   editor-render screen, e
2516   $clear-trace
2517   # start on first line, press ctrl-k
2518   assume-console [
2519     left-click 1, 1
2520     press ctrl-k
2521   ]
2522   run [
2523     editor-event-loop screen, console, e
2524   ]
2525   # cursor deletes to end of line
2526   screen-should-contain [
2527     .          .
2528     .1         .
2529     .456       .
2530     .╌╌╌╌╌╌╌╌╌╌.
2531     .          .
2532   ]
2533   check-trace-count-for-label 9, [print-character]
2534 ]
2535 
2536 after <handle-special-character> [
2537   {
2538     delete-to-end-of-line?:bool <- equal c, 11/ctrl-k
2539     break-unless delete-to-end-of-line?
2540     <begin-delete-to-end-of-line>
2541     deleted-cells:&:duplex-list:char <- delete-to-end-of-line editor
2542     <end-delete-to-end-of-line>
2543     # checks if we can do a minimal render and if we can it will do a minimal render
2544     go-render?:bool <- minimal-render-for-ctrl-k screen, editor, deleted-cells
2545     return
2546   }
2547 ]
2548 
2549 def minimal-render-for-ctrl-k screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
2550   local-scope
2551   load-inputs
2552   # if we deleted nothing, there's nothing to render
2553   return-unless deleted-cells, 0/dont-render
2554   # if the line used to wrap before, give up and render the whole screen
2555   curr-column:num <- get *editor, cursor-column:offset
2556   num-deleted-cells:num <- length deleted-cells
2557   old-row-len:num <- add curr-column, num-deleted-cells
2558   left:num <- get *editor, left:offset
2559   right:num <- get *editor, right:offset
2560   end:num <- subtract right, left
2561   wrap?:bool <- greater-or-equal old-row-len, end
2562   return-if wrap?, 1/go-render
2563   clear-line-until screen, right
2564   return 0/dont-render
2565 ]
2566 
2567 def delete-to-end-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
2568   local-scope
2569   load-inputs
2570   # compute range to delete
2571   start:&:duplex-list:char <- get *editor, before-cursor:offset
2572   end:&:duplex-list:char <- next start
2573   {
2574     at-end-of-text?:bool <- equal end, 0/null
2575     break-if at-end-of-text?
2576     curr:char <- get *end, value:offset
2577     at-end-of-line?:bool <- equal curr, 10/newline
2578     break-if at-end-of-line?
2579     end <- next end
2580     loop
2581   }
2582   # snip it out
2583   result <- next start
2584   remove-between start, end
2585 ]
2586 
2587 scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [
2588   local-scope
2589   assume-screen 10/width, 5/height
2590   s:text <- new [123
2591 456]
2592   e:&:editor <- new-editor s, 0/left, 10/right
2593   editor-render screen, e
2594   $clear-trace
2595   # start on second line (no newline after), press ctrl-k
2596   assume-console [
2597     left-click 2, 1
2598     press ctrl-k
2599   ]
2600   run [
2601     editor-event-loop screen, console, e
2602   ]
2603   # cursor deletes to end of line
2604   screen-should-contain [
2605     .          .
2606     .123       .
2607     .4         .
2608     .╌╌╌╌╌╌╌╌╌╌.
2609     .          .
2610   ]
2611   check-trace-count-for-label 9, [print-character]
2612 ]
2613 
2614 scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [
2615   local-scope
2616   assume-screen 10/width, 5/height
2617   s:text <- new [123
2618 456]
2619   e:&:editor <- new-editor s, 0/left, 10/right
2620   editor-render screen, e
2621   $clear-trace
2622   # start at end of line
2623   assume-console [
2624     left-click 1, 2
2625     press ctrl-k
2626   ]
2627   run [
2628     editor-event-loop screen, console, e
2629   ]
2630   # cursor deletes just last character
2631   screen-should-contain [
2632     .          .
2633     .12        .
2634     .456       .
2635     .╌╌╌╌╌╌╌╌╌╌.
2636     .          .
2637   ]
2638   check-trace-count-for-label 8, [print-character]
2639 ]
2640 
2641 scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [
2642   local-scope
2643   assume-screen 10/width, 5/height
2644   s:text <- new [123
2645 456]
2646   e:&:editor <- new-editor s, 0/left, 10/right
2647   editor-render screen, e
2648   $clear-trace
2649   # start past end of line
2650   assume-console [
2651     left-click 1, 3
2652     press ctrl-k
2653   ]
2654   run [
2655     editor-event-loop screen, console, e
2656   ]
2657   # cursor deletes nothing
2658   screen-should-contain [
2659     .          .
2660     .123       .
2661     .456       .
2662     .╌╌╌╌╌╌╌╌╌╌.
2663     .          .
2664   ]
2665   check-trace-count-for-label 7, [print-character]
2666 ]
2667 
2668 scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [
2669   local-scope
2670   assume-screen 10/width, 5/height
2671   s:text <- new [123
2672 456]
2673   e:&:editor <- new-editor s, 0/left, 10/right
2674   editor-render screen, e
2675   $clear-trace
2676   # start at end of text
2677   assume-console [
2678     left-click 2, 2
2679     press ctrl-k
2680   ]
2681   run [
2682     editor-event-loop screen, console, e
2683   ]
2684   # cursor deletes just the final character
2685   screen-should-contain [
2686     .          .
2687     .123       .
2688     .45        .
2689     .╌╌╌╌╌╌╌╌╌╌.
2690     .          .
2691   ]
2692   check-trace-count-for-label 8, [print-character]
2693 ]
2694 
2695 scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [
2696   local-scope
2697   assume-screen 10/width, 5/height
2698   s:text <- new [123
2699 456]
2700   e:&:editor <- new-editor s, 0/left, 10/right
2701   editor-render screen, e
2702   $clear-trace
2703   # start past end of text
2704   assume-console [
2705     left-click 2, 3
2706     press ctrl-k
2707   ]
2708   run [
2709     editor-event-loop screen, console, e
2710   ]
2711   # cursor deletes nothing
2712   screen-should-contain [
2713     .          .
2714     .123       .
2715     .456       .
2716     .╌╌╌╌╌╌╌╌╌╌.
2717     .          .
2718   ]
2719   # no prints necessary
2720   check-trace-count-for-label 0, [print-character]
2721 ]
2722 
2723 scenario editor-deletes-to-end-of-wrapped-line-with-ctrl-k [
2724   local-scope
2725   assume-screen 10/width, 5/height
2726   # create an editor with the first line wrapping to a second screen row
2727   s:text <- new [1234
2728 567]
2729   e:&:editor <- new-editor s, 0/left, 4/right
2730   editor-render screen, e
2731   $clear-trace
2732   # delete all of the first wrapped line
2733   assume-console [
2734     press ctrl-k
2735   ]
2736   run [
2737     editor-event-loop screen, console, e
2738   ]
2739   # screen shows an empty unwrapped first line
2740   screen-should-contain [
2741     .          .
2742     .          .
2743     .567       .
2744     .╌╌╌╌      .
2745     .          .
2746   ]
2747   # entire screen is refreshed
2748   check-trace-count-for-label 16, [print-character]
2749 ]
2750 
2751 # scroll down if necessary
2752 
2753 scenario editor-can-scroll-down-using-arrow-keys [
2754   local-scope
2755   # screen has 1 line for menu + 3 lines
2756   assume-screen 10/width, 4/height
2757   # initialize editor with >3 lines
2758   s:text <- new [a
2759 b
2760 c
2761 d]
2762   e:&:editor <- new-editor s, 0/left, 10/right
2763   editor-render screen, e
2764   screen-should-contain [
2765     .          .
2766     .a         .
2767     .b         .
2768     .c         .
2769   ]
2770   # position cursor at last line, then try to move further down
2771   assume-console [
2772     left-click 3, 0
2773     press down-arrow
2774   ]
2775   run [
2776     editor-event-loop screen, console, e
2777   ]
2778   # screen slides by one line
2779   screen-should-contain [
2780     .          .
2781     .b         .
2782     .c         .
2783     .d         .
2784   ]
2785 ]
2786 
2787 after <scroll-down> [
2788   trace 10, [app], [scroll down]
2789   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2790   left:num <- get *editor, left:offset
2791   right:num <- get *editor, right:offset
2792   max:num <- subtract right, left
2793   old-top:&:duplex-list:char <- copy top-of-screen
2794   top-of-screen <- before-start-of-next-line top-of-screen, max
2795   *editor <- put *editor, top-of-screen:offset, top-of-screen
2796   no-movement?:bool <- equal old-top, top-of-screen
2797   return-if no-movement?, 0/don't-render
2798 ]
2799 
2800 # Takes a pointer into the doubly-linked list, scans ahead at most 'max'
2801 # positions until the next newline.
2802 # Returns original if no next newline.
2803 # Beware: never return null pointer.
2804 def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [
2805   local-scope
2806   load-inputs
2807   count:num <- copy 0
2808   curr:&:duplex-list:char <- copy original
2809   # skip the initial newline if it exists
2810   {
2811     c:char <- get *curr, value:offset
2812     at-newline?:bool <- equal c, 10/newline
2813     break-unless at-newline?
2814     curr <- next curr
2815     count <- add count, 1
2816   }
2817   {
2818     return-unless curr, original
2819     done?:bool <- greater-or-equal count, max
2820     break-if done?
2821     c:char <- get *curr, value:offset
2822     at-newline?:bool <- equal c, 10/newline
2823     break-if at-newline?
2824     curr <- next curr
2825     count <- add count, 1
2826     loop
2827   }
2828   return-unless curr, original
2829   return curr
2830 ]
2831 
2832 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys [
2833   local-scope
2834   # screen has 1 line for menu + 3 lines
2835   assume-screen 10/width, 4/height
2836   # initialize editor with a long, wrapped line and more than a screen of
2837   # other lines
2838   s:text <- new [abcdef
2839 g
2840 h
2841 i]
2842   e:&:editor <- new-editor s, 0/left, 5/right
2843   editor-render screen, e
2844   screen-should-contain [
2845     .          .
2846     .abcd↩     .
2847     .ef        .
2848     .g         .
2849   ]
2850   # position cursor at last line, then try to move further down
2851   assume-console [
2852     left-click 3, 0
2853     press down-arrow
2854   ]
2855   run [
2856     editor-event-loop screen, console, e
2857   ]
2858   # screen shows partial wrapped line
2859   screen-should-contain [
2860     .          .
2861     .ef        .
2862     .g         .
2863     .h         .
2864   ]
2865 ]
2866 
2867 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys-2 [
2868   local-scope
2869   # screen has 1 line for menu + 3 lines
2870   assume-screen 10/width, 4/height
2871   # editor starts with a long line wrapping twice
2872   s:text <- new [abcdefghij
2873 k
2874 l
2875 m]
2876   e:&:editor <- new-editor s, 0/left, 5/right
2877   # position cursor at last line, then try to move further down
2878   assume-console [
2879     left-click 3, 0
2880     press down-arrow
2881   ]
2882   run [
2883     editor-event-loop screen, console, e
2884   ]
2885   # screen shows partial wrapped line containing a wrap icon
2886   screen-should-contain [
2887     .          .
2888     .efgh↩     .
2889     .ij        .
2890     .k         .
2891   ]
2892   # scroll down again
2893   assume-console [
2894     press down-arrow
2895   ]
2896   run [
2897     editor-event-loop screen, console, e
2898   ]
2899   # screen shows partial wrapped line
2900   screen-should-contain [
2901     .          .
2902     .ij        .
2903     .k         .
2904     .l         .
2905   ]
2906 ]
2907 
2908 scenario editor-scrolls-down-when-line-wraps [
2909   local-scope
2910   # screen has 1 line for menu + 3 lines
2911   assume-screen 5/width, 4/height
2912   # editor contains a long line in the third line
2913   s:text <- new [a
2914 b
2915 cdef]
2916   e:&:editor <- new-editor s, 0/left, 5/right
2917   # position cursor at end, type a character
2918   assume-console [
2919     left-click 3, 4
2920     type [g]
2921   ]
2922   run [
2923     editor-event-loop screen, console, e
2924     3:num/raw <- get *e, cursor-row:offset
2925     4:num/raw <- get *e, cursor-column:offset
2926   ]
2927   # screen scrolls
2928   screen-should-contain [
2929     .     .
2930     .b    .
2931     .cdef↩.
2932     .g    .
2933   ]
2934   memory-should-contain [
2935     3 <- 3
2936     4 <- 1
2937   ]
2938 ]
2939 
2940 scenario editor-stops-scrolling-once-bottom-is-visible [
2941   local-scope
2942   # screen has 1 line for menu + 3 lines
2943   assume-screen 10/width, 4/height
2944   # initialize editor with 2 lines
2945   s:text <- new [a
2946 b]
2947   e:&:editor <- new-editor s, 0/left, 10/right
2948   editor-render screen, e
2949   screen-should-contain [
2950     .          .
2951     .a         .
2952     .b         .
2953     .╌╌╌╌╌╌╌╌╌╌.
2954   ]
2955   # position cursor at last line, then try to move further down
2956   assume-console [
2957     left-click 3, 0
2958     press down-arrow
2959   ]
2960   run [
2961     editor-event-loop screen, console, e
2962   ]
2963   # no change since the bottom border was already visible
2964   screen-should-contain [
2965     .          .
2966     .a         .
2967     .b         .
2968     .╌╌╌╌╌╌╌╌╌╌.
2969   ]
2970 ]
2971 
2972 scenario editor-scrolls-down-on-newline [
2973   local-scope
2974   assume-screen 5/width, 4/height
2975   # position cursor after last line and type newline
2976   s:text <- new [a
2977 b
2978 c]
2979   e:&:editor <- new-editor s, 0/left, 5/right
2980   assume-console [
2981     left-click 3, 4
2982     type [
2983 ]
2984   ]
2985   run [
2986     editor-event-loop screen, console, e
2987     3:num/raw <- get *e, cursor-row:offset
2988     4:num/raw <- get *e, cursor-column:offset
2989   ]
2990   # screen scrolls
2991   screen-should-contain [
2992     .     .
2993     .b    .
2994     .c    .
2995     .     .
2996   ]
2997   memory-should-contain [
2998     3 <- 3
2999     4 <- 0
3000   ]
3001 ]
3002 
3003 scenario editor-scrolls-down-on-right-arrow [
3004   local-scope
3005   # screen has 1 line for menu + 3 lines
3006   assume-screen 5/width, 4/height
3007   # editor contains a wrapped line
3008   s:text <- new [a
3009 b
3010 cdefgh]
3011   e:&:editor <- new-editor s, 0/left, 5/right
3012   # position cursor at end of screen and try to move right
3013   assume-console [
3014     left-click 3, 3
3015     press right-arrow
3016   ]
3017   run [
3018     editor-event-loop screen, console, e
3019     3:num/raw <- get *e, cursor-row:offset
3020     4:num/raw <- get *e, cursor-column:offset
3021   ]
3022   # screen scrolls
3023   screen-should-contain [
3024     .     .
3025     .b    .
3026     .cdef↩.
3027     .gh   .
3028   ]
3029   memory-should-contain [
3030     3 <- 3
3031     4 <- 0
3032   ]
3033 ]
3034 
3035 scenario editor-scrolls-down-on-right-arrow-2 [
3036   local-scope
3037   # screen has 1 line for menu + 3 lines
3038   assume-screen 5/width, 4/height
3039   # editor contains more lines than can fit on screen
3040   s:text <- new [a
3041 b
3042 c
3043 d]
3044   e:&:editor <- new-editor s, 0/left, 5/right
3045   # position cursor at end of screen and try to move right
3046   assume-console [
3047     left-click 3, 3
3048     press right-arrow
3049   ]
3050   run [
3051     editor-event-loop screen, console, e
3052     3:num/raw <- get *e, cursor-row:offset
3053     4:num/raw <- get *e, cursor-column:offset
3054   ]
3055   # screen scrolls
3056   screen-should-contain [
3057     .     .
3058     .b    .
3059     .c    .
3060     .d    .
3061   ]
3062   memory-should-contain [
3063     3 <- 3
3064     4 <- 0
3065   ]
3066 ]
3067 
3068 scenario editor-scrolls-at-end-on-down-arrow [
3069   local-scope
3070   assume-screen 10/width, 5/height
3071   s:text <- new [abc
3072 de]
3073   e:&:editor <- new-editor s, 0/left, 10/right
3074   editor-render screen, e
3075   $clear-trace
3076   # try to move down past end of text
3077   assume-console [
3078     left-click 2, 0
3079     press down-arrow
3080   ]
3081   run [
3082     editor-event-loop screen, console, e
3083     3:num/raw <- get *e, cursor-row:offset
3084     4:num/raw <- get *e, cursor-column:offset
3085   ]
3086   # no change
3087   memory-should-contain [
3088     3 <- 2
3089     4 <- 0
3090   ]
3091 ]
3092 
3093 scenario editor-combines-page-and-line-scroll [
3094   local-scope
3095   # screen has 1 line for menu + 3 lines
3096   assume-screen 10/width, 4/height
3097   # initialize editor with a few pages of lines
3098   s:text <- new [a
3099 b
3100 c
3101 d
3102 e
3103 f
3104 g]
3105   e:&:editor <- new-editor s, 0/left, 5/right
3106   editor-render screen, e
3107   # scroll down one page and one line
3108   assume-console [
3109     press page-down
3110     left-click 3, 0
3111     press down-arrow
3112   ]
3113   run [
3114     editor-event-loop screen, console, e
3115   ]
3116   # screen scrolls down 3 lines
3117   screen-should-contain [
3118     .          .
3119     .d         .
3120     .e         .
3121     .f         .
3122   ]
3123 ]
3124 
3125 # scroll up if necessary
3126 
3127 scenario editor-can-scroll-up-using-arrow-keys [
3128   local-scope
3129   # screen has 1 line for menu + 3 lines
3130   assume-screen 10/width, 4/height
3131   # initialize editor with >3 lines
3132   s:text <- new [a
3133 b
3134 c
3135 d]
3136   e:&:editor <- new-editor s, 0/left, 10/right
3137   editor-render screen, e
3138   screen-should-contain [
3139     .          .
3140     .a         .
3141     .b         .
3142     .c         .
3143   ]
3144   # position cursor at top of second page, then try to move up
3145   assume-console [
3146     press page-down
3147     press up-arrow
3148   ]
3149   run [
3150     editor-event-loop screen, console, e
3151   ]
3152   # screen slides by one line
3153   screen-should-contain [
3154     .          .
3155     .b         .
3156     .c         .
3157     .d         .
3158   ]
3159 ]
3160 
3161 after <scroll-up> [
3162   trace 10, [app], [scroll up]
3163   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3164   old-top:&:duplex-list:char <- copy top-of-screen
3165   top-of-screen <- before-previous-screen-line top-of-screen, editor
3166   *editor <- put *editor, top-of-screen:offset, top-of-screen
3167   no-movement?:bool <- equal old-top, top-of-screen
3168   return-if no-movement?, 0/don't-render
3169 ]
3170 
3171 # Takes a pointer into the doubly-linked list, scans back to before start of
3172 # previous *wrapped* line.
3173 # Returns original if no next newline.
3174 # Beware: never return null pointer.
3175 def before-previous-screen-line in:&:duplex-list:char, editor:&:editor -> out:&:duplex-list:char [
3176   local-scope
3177   load-inputs
3178   curr:&:duplex-list:char <- copy in
3179   c:char <- get *curr, value:offset
3180   # compute max, number of characters to skip
3181   #   1 + len%(width-1)
3182   #   except rotate second term to vary from 1 to width-1 rather than 0 to width-2
3183   left:num <- get *editor, left:offset
3184   right:num <- get *editor, right:offset
3185   max-line-length:num <- subtract right, left, -1/exclusive-right, 1/wrap-icon
3186   sentinel:&:duplex-list:char <- get *editor, data:offset
3187   len:num <- previous-line-length curr, sentinel
3188   {
3189     break-if len
3190     # empty line; just skip this newline
3191     prev:&:duplex-list:char <- prev curr
3192     return-unless prev, curr
3193     return prev
3194   }
3195   _, max:num <- divide-with-remainder len, max-line-length
3196   # remainder 0 => scan one width-worth
3197   {
3198     break-if max
3199     max <- copy max-line-length
3200   }
3201   max <- add max, 1
3202   count:num <- copy 0
3203   # skip 'max' characters
3204   {
3205     done?:bool <- greater-or-equal count, max
3206     break-if done?
3207     prev:&:duplex-list:char <- prev curr
3208     break-unless prev
3209     curr <- copy prev
3210     count <- add count, 1
3211     loop
3212   }
3213   return curr
3214 ]
3215 
3216 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys [
3217   local-scope
3218   # screen has 1 line for menu + 3 lines
3219   assume-screen 10/width, 4/height
3220   # initialize editor with a long, wrapped line and more than a screen of
3221   # other lines
3222   s:text <- new [abcdef
3223 g
3224 h
3225 i]
3226   e:&:editor <- new-editor s, 0/left, 5/right
3227   editor-render screen, e
3228   screen-should-contain [
3229     .          .
3230     .abcd↩     .
3231     .ef        .
3232     .g         .
3233   ]
3234   # position cursor at top of second page, just below wrapped line
3235   assume-console [
3236     press page-down
3237   ]
3238   run [
3239     editor-event-loop screen, console, e
3240   ]
3241   screen-should-contain [
3242     .          .
3243     .g         .
3244     .h         .
3245     .i         .
3246   ]
3247   # now move up one line
3248   assume-console [
3249     press up-arrow
3250   ]
3251   run [
3252     editor-event-loop screen, console, e
3253   ]
3254   # screen shows partial wrapped line
3255   screen-should-contain [
3256     .          .
3257     .ef        .
3258     .g         .
3259     .h         .
3260   ]
3261 ]
3262 
3263 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-2 [
3264   local-scope
3265   # screen has 1 line for menu + 4 lines
3266   assume-screen 10/width, 5/height
3267   # editor starts with a long line wrapping twice, occupying 3 of the 4 lines
3268   s:text <- new [abcdefghij
3269 k
3270 l
3271 m]
3272   e:&:editor <- new-editor s, 0/left, 5/right
3273   editor-render screen, e
3274   # position cursor at top of second page
3275   assume-console [
3276     press page-down
3277   ]
3278   run [
3279     editor-event-loop screen, console, e
3280   ]
3281   screen-should-contain [
3282     .          .
3283     .k         .
3284     .l         .
3285     .m         .
3286     .╌╌╌╌╌     .
3287   ]
3288   # move up one line
3289   assume-console [
3290     press up-arrow
3291   ]
3292   run [
3293     editor-event-loop screen, console, e
3294   ]
3295   # screen shows partial wrapped line
3296   screen-should-contain [
3297     .          .
3298     .ij        .
3299     .k         .
3300     .l         .
3301     .m         .
3302   ]
3303   # move up a second line
3304   assume-console [
3305     press up-arrow
3306   ]
3307   run [
3308     editor-event-loop screen, console, e
3309   ]
3310   # screen shows partial wrapped line
3311   screen-should-contain [
3312     .          .
3313     .efgh↩     .
3314     .ij        .
3315     .k         .
3316     .l         .
3317   ]
3318   # move up a third line
3319   assume-console [
3320     press up-arrow
3321   ]
3322   run [
3323     editor-event-loop screen, console, e
3324   ]
3325   # screen shows partial wrapped line
3326   screen-should-contain [
3327     .          .
3328     .abcd↩     .
3329     .efgh↩     .
3330     .ij        .
3331     .k         .
3332   ]
3333 ]
3334 
3335 # same as editor-scrolls-up-past-wrapped-line-using-arrow-keys but length
3336 # slightly off, just to prevent over-training
3337 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-3 [
3338   local-scope
3339   # screen has 1 line for menu + 3 lines
3340   assume-screen 10/width, 4/height
3341   # initialize editor with a long, wrapped line and more than a screen of
3342   # other lines
3343   s:text <- new [abcdef
3344 g
3345 h
3346 i]
3347   e:&:editor <- new-editor s, 0/left, 6/right
3348   editor-render screen, e
3349   screen-should-contain [
3350     .          .
3351     .abcde↩    .
3352     .f         .
3353     .g         .
3354   ]
3355   # position cursor at top of second page, just below wrapped line
3356   assume-console [
3357     press page-down
3358   ]
3359   run [
3360     editor-event-loop screen, console, e
3361   ]
3362   screen-should-contain [
3363     .          .
3364     .g         .
3365     .h         .
3366     .i         .
3367   ]
3368   # now move up one line
3369   assume-console [
3370     press up-arrow
3371   ]
3372   run [
3373     editor-event-loop screen, console, e
3374   ]
3375   # screen shows partial wrapped line
3376   screen-should-contain [
3377     .          .
3378     .f         .
3379     .g         .
3380     .h         .
3381   ]
3382 ]
3383 
3384 # check empty lines
3385 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-4 [
3386   local-scope
3387   assume-screen 10/width, 4/height
3388   # initialize editor with some lines around an empty line
3389   s:text <- new [a
3390 b
3391 
3392 c
3393 d
3394 e]
3395   e:&:editor <- new-editor s, 0/left, 6/right
3396   editor-render screen, e
3397   assume-console [
3398     press page-down
3399   ]
3400   run [
3401     editor-event-loop screen, console, e
3402   ]
3403   screen-should-contain [
3404     .          .
3405     .          .
3406     .c         .
3407     .d         .
3408   ]
3409   assume-console [
3410     press page-down
3411   ]
3412   run [
3413     editor-event-loop screen, console, e
3414   ]
3415   screen-should-contain [
3416     .          .
3417     .d         .
3418     .e         .
3419     .╌╌╌╌╌╌    .
3420   ]
3421   assume-console [
3422     press page-up
3423   ]
3424   run [
3425     editor-event-loop screen, console, e
3426   ]
3427   screen-should-contain [
3428     .          .
3429     .          .
3430     .c         .
3431     .d         .
3432   ]
3433 ]
3434 
3435 scenario editor-scrolls-up-on-left-arrow [
3436   local-scope
3437   # screen has 1 line for menu + 3 lines
3438   assume-screen 5/width, 4/height
3439   # editor contains >3 lines
3440   s:text <- new [a
3441 b
3442 c
3443 d
3444 e]
3445   e:&:editor <- new-editor s, 0/left, 5/right
3446   editor-render screen, e
3447   # position cursor at top of second page
3448   assume-console [
3449     press page-down
3450   ]
3451   run [
3452     editor-event-loop screen, console, e
3453   ]
3454   screen-should-contain [
3455     .     .
3456     .c    .
3457     .d    .
3458     .e    .
3459   ]
3460   # now try to move left
3461   assume-console [
3462     press left-arrow
3463   ]
3464   run [
3465     editor-event-loop screen, console, e
3466     3:num/raw <- get *e, cursor-row:offset
3467     4:num/raw <- get *e, cursor-column:offset
3468   ]
3469   # screen scrolls
3470   screen-should-contain [
3471     .     .
3472     .b    .
3473     .c    .
3474     .d    .
3475   ]
3476   memory-should-contain [
3477     3 <- 1
3478     4 <- 1
3479   ]
3480 ]
3481 
3482 scenario editor-can-scroll-up-to-start-of-file [
3483   local-scope
3484   # screen has 1 line for menu + 3 lines
3485   assume-screen 10/width, 4/height
3486   # initialize editor with >3 lines
3487   s:text <- new [a
3488 b
3489 c
3490 d]
3491   e:&:editor <- new-editor s, 0/left, 10/right
3492   editor-render screen, e
3493   screen-should-contain [
3494     .          .
3495     .a         .
3496     .b         .
3497     .c         .
3498   ]
3499   # position cursor at top of second page, then try to move up to start of
3500   # text
3501   assume-console [
3502     press page-down
3503     press up-arrow
3504     press up-arrow
3505   ]
3506   run [
3507     editor-event-loop screen, console, e
3508   ]
3509   # screen slides by one line
3510   screen-should-contain [
3511     .          .
3512     .a         .
3513     .b         .
3514     .c         .
3515   ]
3516   # try to move up again
3517   assume-console [
3518     press up-arrow
3519   ]
3520   run [
3521     editor-event-loop screen, console, e
3522   ]
3523   # screen remains unchanged
3524   screen-should-contain [
3525     .          .
3526     .a         .
3527     .b         .
3528     .c         .
3529   ]
3530 ]
3531 
3532 # ctrl-f/page-down - render next page if it exists
3533 
3534 scenario editor-can-scroll [
3535   local-scope
3536   assume-screen 10/width, 4/height
3537   s:text <- new [a
3538 b
3539 c
3540 d]
3541   e:&:editor <- new-editor s, 0/left, 10/right
3542   editor-render screen, e
3543   screen-should-contain [
3544     .          .
3545     .a         .
3546     .b         .
3547     .c         .
3548   ]
3549   # scroll down
3550   assume-console [
3551     press page-down
3552   ]
3553   run [
3554     editor-event-loop screen, console, e
3555   ]
3556   # screen shows next page
3557   screen-should-contain [
3558     .          .
3559     .c         .
3560     .d         .
3561     .╌╌╌╌╌╌╌╌╌╌.
3562   ]
3563 ]
3564 
3565 after <handle-special-character> [
3566   {
3567     page-down?:bool <- equal c, 6/ctrl-f
3568     break-unless page-down?
3569     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3570     <begin-move-cursor>
3571     page-down editor
3572     undo-coalesce-tag:num <- copy 0/never
3573     <end-move-cursor>
3574     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3575     movement?:bool <- not-equal top-of-screen, old-top
3576     return movement?/go-render
3577   }
3578 ]
3579 
3580 after <handle-special-key> [
3581   {
3582     page-down?:bool <- equal k, 65518/page-down
3583     break-unless page-down?
3584     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3585     <begin-move-cursor>
3586     page-down editor
3587     undo-coalesce-tag:num <- copy 0/never
3588     <end-move-cursor>
3589     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3590     movement?:bool <- not-equal top-of-screen, old-top
3591     return movement?/go-render
3592   }
3593 ]
3594 
3595 # page-down skips entire wrapped lines, so it can't scroll past lines
3596 # taking up the entire screen
3597 def page-down editor:&:editor -> editor:&:editor [
3598   local-scope
3599   load-inputs
3600   # if editor contents don't overflow screen, do nothing
3601   bottom-of-screen:&:duplex-list:char <- get *editor, bottom-of-screen:offset
3602   return-unless bottom-of-screen
3603   # if not, position cursor at final character
3604   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
3605   before-cursor:&:duplex-list:char <- prev bottom-of-screen
3606   *editor <- put *editor, before-cursor:offset, before-cursor
3607   # keep one line in common with previous page
3608   {
3609     last:char <- get *before-cursor, value:offset
3610     newline?:bool <- equal last, 10/newline
3611     break-unless newline?:bool
3612     before-cursor <- prev before-cursor
3613     *editor <- put *editor, before-cursor:offset, before-cursor
3614   }
3615   # move cursor and top-of-screen to start of that line
3616   move-to-start-of-line editor
3617   before-cursor <- get *editor, before-cursor:offset
3618   *editor <- put *editor, top-of-screen:offset, before-cursor
3619 ]
3620 
3621 # jump to previous newline
3622 def move-to-start-of-line editor:&:editor -> editor:&:editor [
3623   local-scope
3624   load-inputs
3625   # update cursor column
3626   left:num <- get *editor, left:offset
3627   cursor-column:num <- copy left
3628   *editor <- put *editor, cursor-column:offset, cursor-column
3629   # update before-cursor
3630   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
3631   init:&:duplex-list:char <- get *editor, data:offset
3632   # while not at start of line, move
3633   {
3634     at-start-of-text?:bool <- equal before-cursor, init
3635     break-if at-start-of-text?
3636     prev:char <- get *before-cursor, value:offset
3637     at-start-of-line?:bool <- equal prev, 10/newline
3638     break-if at-start-of-line?
3639     before-cursor <- prev before-cursor
3640     *editor <- put *editor, before-cursor:offset, before-cursor
3641     assert before-cursor, [move-to-start-of-line tried to move before start of text]
3642     loop
3643   }
3644 ]
3645 
3646 scenario editor-does-not-scroll-past-end [
3647   local-scope
3648   assume-screen 10/width, 4/height
3649   s:text <- new [a
3650 b]
3651   e:&:editor <- new-editor s, 0/left, 10/right
3652   editor-render screen, e
3653   screen-should-contain [
3654     .          .
3655     .a         .
3656     .b         .
3657     .╌╌╌╌╌╌╌╌╌╌.
3658   ]
3659   # scroll down
3660   assume-console [
3661     press page-down
3662   ]
3663   run [
3664     editor-event-loop screen, console, e
3665   ]
3666   # screen remains unmodified
3667   screen-should-contain [
3668     .          .
3669     .a         .
3670     .b         .
3671     .╌╌╌╌╌╌╌╌╌╌.
3672   ]
3673 ]
3674 
3675 scenario editor-starts-next-page-at-start-of-wrapped-line [
3676   local-scope
3677   # screen has 1 line for menu + 3 lines for text
3678   assume-screen 10/width, 4/height
3679   # editor contains a long last line
3680   s:text <- new [a
3681 b
3682 cdefgh]
3683   # editor screen triggers wrap of last line
3684   e:&:editor <- new-editor s, 0/left, 4/right
3685   editor-render screen, e
3686   # some part of last line is not displayed
3687   screen-should-contain [
3688     .          .
3689     .a         .
3690     .b         .
3691     .cde↩      .
3692   ]
3693   # scroll down
3694   assume-console [
3695     press page-down
3696   ]
3697   run [
3698     editor-event-loop screen, console, e
3699   ]
3700   # screen shows entire wrapped line
3701   screen-should-contain [
3702     .          .
3703     .cde↩      .
3704     .fgh       .
3705     .╌╌╌╌      .
3706   ]
3707 ]
3708 
3709 scenario editor-starts-next-page-at-start-of-wrapped-line-2 [
3710   local-scope
3711   # screen has 1 line for menu + 3 lines for text
3712   assume-screen 10/width, 4/height
3713   # editor contains a very long line that occupies last two lines of screen
3714   # and still has something left over
3715   s:text <- new [a
3716 bcdefgh]
3717   e:&:editor <- new-editor s, 0/left, 4/right
3718   editor-render screen, e
3719   # some part of last line is not displayed
3720   screen-should-contain [
3721     .          .
3722     .a         .
3723     .bcd↩      .
3724     .efg↩      .
3725   ]
3726   # scroll down
3727   assume-console [
3728     press page-down
3729   ]
3730   run [
3731     editor-event-loop screen, console, e
3732   ]
3733   # screen shows entire wrapped line
3734   screen-should-contain [
3735     .          .
3736     .bcd↩      .
3737     .efg↩      .
3738     .h         .
3739   ]
3740 ]
3741 
3742 # ctrl-b/page-up - render previous page if it exists
3743 
3744 scenario editor-can-scroll-up [
3745   local-scope
3746   assume-screen 10/width, 4/height
3747   s:text <- new [a
3748 b
3749 c
3750 d]
3751   e:&:editor <- new-editor s, 0/left, 10/right
3752   editor-render screen, e
3753   screen-should-contain [
3754     .          .
3755     .a         .
3756     .b         .
3757     .c         .
3758   ]
3759   # scroll down
3760   assume-console [
3761     press page-down
3762   ]
3763   run [
3764     editor-event-loop screen, console, e
3765   ]
3766   # screen shows next page
3767   screen-should-contain [
3768     .          .
3769     .c         .
3770     .d         .
3771     .╌╌╌╌╌╌╌╌╌╌.
3772   ]
3773   # scroll back up
3774   assume-console [
3775     press page-up
3776   ]
3777   run [
3778     editor-event-loop screen, console, e
3779   ]
3780   # screen shows original page again
3781   screen-should-contain [
3782     .          .
3783     .a         .
3784     .b         .
3785     .c         .
3786   ]
3787 ]
3788 
3789 after <handle-special-character> [
3790   {
3791     page-up?:bool <- equal c, 2/ctrl-b
3792     break-unless page-up?
3793     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3794     <begin-move-cursor>
3795     editor <- page-up editor, screen-height
3796     undo-coalesce-tag:num <- copy 0/never
3797     <end-move-cursor>
3798     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3799     movement?:bool <- not-equal top-of-screen, old-top
3800     return movement?/go-render
3801   }
3802 ]
3803 
3804 after <handle-special-key> [
3805   {
3806     page-up?:bool <- equal k, 65519/page-up
3807     break-unless page-up?
3808     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3809     <begin-move-cursor>
3810     editor <- page-up editor, screen-height
3811     undo-coalesce-tag:num <- copy 0/never
3812     <end-move-cursor>
3813     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3814     movement?:bool <- not-equal top-of-screen, old-top
3815     # don't bother re-rendering if nothing changed. todo: test this
3816     return movement?/go-render
3817   }
3818 ]
3819 
3820 def page-up editor:&:editor, screen-height:num -> editor:&:editor [
3821   local-scope
3822   load-inputs
3823   max:num <- subtract screen-height, 1/menu-bar, 1/overlapping-line
3824   count:num <- copy 0
3825   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3826   {
3827     done?:bool <- greater-or-equal count, max
3828     break-if done?
3829     prev:&:duplex-list:char <- before-previous-screen-line top-of-screen, editor
3830     break-unless prev
3831     top-of-screen <- copy prev
3832     *editor <- put *editor, top-of-screen:offset, top-of-screen
3833     count <- add count, 1
3834     loop
3835   }
3836 ]
3837 
3838 scenario editor-can-scroll-up-multiple-pages [
3839   local-scope
3840   # screen has 1 line for menu + 3 lines
3841   assume-screen 10/width, 4/height
3842   # initialize editor with 8 lines
3843   s:text <- new [a
3844 b
3845 c
3846 d
3847 e
3848 f
3849 g
3850 h]
3851   e:&:editor <- new-editor s, 0/left, 10/right
3852   editor-render screen, e
3853   screen-should-contain [
3854     .          .
3855     .a         .
3856     .b         .
3857     .c         .
3858   ]
3859   # scroll down two pages
3860   assume-console [
3861     press page-down
3862     press page-down
3863   ]
3864   run [
3865     editor-event-loop screen, console, e
3866   ]
3867   # screen shows third page
3868   screen-should-contain [
3869     .          .
3870     .e         .
3871     .f         .
3872     .g         .
3873   ]
3874   # scroll up
3875   assume-console [
3876     press page-up
3877   ]
3878   run [
3879     editor-event-loop screen, console, e
3880   ]
3881   # screen shows second page
3882   screen-should-contain [
3883     .          .
3884     .c         .
3885     .d         .
3886     .e         .
3887   ]
3888   # scroll up again
3889   assume-console [
3890     press page-up
3891   ]
3892   run [
3893     editor-event-loop screen, console, e
3894   ]
3895   # screen shows original page again
3896   screen-should-contain [
3897     .          .
3898     .a         .
3899     .b         .
3900     .c         .
3901   ]
3902 ]
3903 
3904 scenario editor-can-scroll-up-wrapped-lines [
3905   local-scope
3906   # screen has 1 line for menu + 5 lines for text
3907   assume-screen 10/width, 6/height
3908   # editor contains a long line in the first page
3909   s:text <- new [a
3910 b
3911 cdefgh
3912 i
3913 j
3914 k
3915 l
3916 m
3917 n
3918 o]
3919   # editor screen triggers wrap of last line
3920   e:&:editor <- new-editor s, 0/left, 4/right
3921   editor-render screen, e
3922   # some part of last line is not displayed
3923   screen-should-contain [
3924     .          .
3925     .a         .
3926     .b         .
3927     .cde↩      .
3928     .fgh       .
3929     .i         .
3930   ]
3931   # scroll down a page and a line
3932   assume-console [
3933     press page-down
3934     left-click 5, 0
3935     press down-arrow
3936   ]
3937   run [
3938     editor-event-loop screen, console, e
3939   ]
3940   # screen shows entire wrapped line
3941   screen-should-contain [
3942     .          .
3943     .j         .
3944     .k         .
3945     .l         .
3946     .m         .
3947     .n         .
3948   ]
3949   # now scroll up one page
3950   assume-console [
3951     press page-up
3952   ]
3953   run [
3954     editor-event-loop screen, console, e
3955   ]
3956   # screen resets
3957   screen-should-contain [
3958     .          .
3959     .b         .
3960     .cde↩      .
3961     .fgh       .
3962     .i         .
3963     .j         .
3964   ]
3965 ]
3966 
3967 scenario editor-can-scroll-up-wrapped-lines-2 [
3968   local-scope
3969   # screen has 1 line for menu + 3 lines for text
3970   assume-screen 10/width, 4/height
3971   # editor contains a very long line that occupies last two lines of screen
3972   # and still has something left over
3973   s:text <- new [a
3974 bcdefgh]
3975   e:&:editor <- new-editor s, 0/left, 4/right
3976   editor-render screen, e
3977   # some part of last line is not displayed
3978   screen-should-contain [
3979     .          .
3980     .a         .
3981     .bcd↩      .
3982     .efg↩      .
3983   ]
3984   # scroll down
3985   assume-console [
3986     press page-down
3987   ]
3988   run [
3989     editor-event-loop screen, console, e
3990   ]
3991   # screen shows entire wrapped line
3992   screen-should-contain [
3993     .          .
3994     .bcd↩      .
3995     .efg↩      .
3996     .h         .
3997   ]
3998   # scroll back up
3999   assume-console [
4000     press page-up
4001   ]
4002   run [
4003     editor-event-loop screen, console, e
4004   ]
4005   # screen resets
4006   screen-should-contain [
4007     .          .
4008     .a         .
4009     .bcd↩      .
4010     .efg↩      .
4011   ]
4012 ]
4013 
4014 scenario editor-can-scroll-up-past-nonempty-lines [
4015   local-scope
4016   assume-screen 10/width, 4/height
4017   # text with empty line in second screen
4018   s:text <- new [axx
4019 bxx
4020 cxx
4021 dxx
4022 exx
4023 fxx
4024 gxx
4025 hxx
4026 ]
4027   e:&:editor <- new-editor s, 0/left, 4/right
4028   editor-render screen, e
4029   screen-should-contain [
4030     .          .
4031     .axx       .
4032     .bxx       .
4033     .cxx       .
4034   ]
4035   assume-console [
4036     press page-down
4037   ]
4038   run [
4039     editor-event-loop screen, console, e
4040   ]
4041   screen-should-contain [
4042     .          .
4043     .cxx       .
4044     .dxx       .
4045     .exx       .
4046   ]
4047   assume-console [
4048     press page-down
4049   ]
4050   run [
4051     editor-event-loop screen, console, e
4052   ]
4053   screen-should-contain [
4054     .          .
4055     .exx       .
4056     .fxx       .
4057     .gxx       .
4058   ]
4059   # scroll back up past empty line
4060   assume-console [
4061     press page-up
4062   ]
4063   run [
4064     editor-event-loop screen, console, e
4065   ]
4066   screen-should-contain [
4067     .          .
4068     .cxx       .
4069     .dxx       .
4070     .exx       .
4071   ]
4072 ]
4073 
4074 scenario editor-can-scroll-up-past-empty-lines [
4075   local-scope
4076   assume-screen 10/width, 4/height
4077   # text with empty line in second screen
4078   s:text <- new [axy
4079 bxy
4080 cxy
4081 
4082 dxy
4083 exy
4084 fxy
4085 gxy
4086 ]
4087   e:&:editor <- new-editor s, 0/left, 4/right
4088   editor-render screen, e
4089   screen-should-contain [
4090     .          .
4091     .axy       .
4092     .bxy       .
4093     .cxy       .
4094   ]
4095   assume-console [
4096     press page-down
4097   ]
4098   run [
4099     editor-event-loop screen, console, e
4100   ]
4101   screen-should-contain [
4102     .          .
4103     .cxy       .
4104     .          .
4105     .dxy       .
4106   ]
4107   assume-console [
4108     press page-down
4109   ]
4110   run [
4111     editor-event-loop screen, console, e
4112   ]
4113   screen-should-contain [
4114     .          .
4115     .dxy       .
4116     .exy       .
4117     .fxy       .
4118   ]
4119   # scroll back up past empty line
4120   assume-console [
4121     press page-up
4122   ]
4123   run [
4124     editor-event-loop screen, console, e
4125   ]
4126   screen-should-contain [
4127     .          .
4128     .cxy       .
4129     .          .
4130     .dxy       .
4131   ]
4132 ]
4133 
4134 # ctrl-s - scroll up by one line
4135 # todo: scenarios
4136 
4137 after <handle-special-character> [
4138   {
4139     scroll-up?:bool <- equal c, 19/ctrl-s
4140     break-unless scroll-up?
4141     <begin-move-cursor>
4142     go-render?:bool, editor <- line-up editor, screen-height
4143     undo-coalesce-tag:num <- copy 5/line-up
4144     <end-move-cursor>
4145     return go-render?
4146   }
4147 ]
4148 
4149 def line-up editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
4150   local-scope
4151   load-inputs
4152   left:num <- get *editor, left:offset
4153   right:num <- get *editor, right:offset
4154   max:num <- subtract right, left
4155   old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4156   new-top:&:duplex-list:char <- before-start-of-next-line old-top, max
4157   movement?:bool <- not-equal old-top, new-top
4158   {
4159     break-unless movement?
4160     *editor <- put *editor, top-of-screen:offset, new-top
4161   }
4162   return movement?
4163 ]
4164 
4165 # ctrl-x - scroll down by one line
4166 # todo: scenarios
4167 
4168 after <handle-special-character> [
4169   {
4170     scroll-down?:bool <- equal c, 24/ctrl-x
4171     break-unless scroll-down?
4172     <begin-move-cursor>
4173     go-render?:bool, editor <- line-down editor, screen-height
4174     undo-coalesce-tag:num <- copy 6/line-down
4175     <end-move-cursor>
4176     return go-render?
4177   }
4178 ]
4179 
4180 def line-down editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
4181   local-scope
4182   load-inputs
4183   old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4184   new-top:&:duplex-list:char <- before-previous-screen-line old-top, editor
4185   movement?:bool <- not-equal old-top, new-top
4186   {
4187     break-unless movement?
4188     *editor <- put *editor, top-of-screen:offset, new-top
4189   }
4190   return movement?
4191 ]
4192 
4193 # ctrl-t - move current line to top of screen
4194 # todo: scenarios
4195 
4196 after <handle-special-character> [
4197   {
4198     scroll-down?:bool <- equal c, 20/ctrl-t
4199     break-unless scroll-down?
4200     <begin-move-cursor>
4201     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4202     cursor:&:duplex-list:char <- get *editor, before-cursor:offset
4203     cursor <- next cursor
4204     new-top:&:duplex-list:char <- before-previous-screen-line cursor, editor
4205     *editor <- put *editor, top-of-screen:offset, new-top
4206     *editor <- put *editor, cursor-row:offset, 1
4207     go-render?:bool <- not-equal new-top, old-top
4208     undo-coalesce-tag:num <- copy 0/never
4209     <end-move-cursor>
4210     return go-render?
4211   }
4212 ]
4213 
4214 # ctrl-/ - comment/uncomment current line
4215 
4216 after <handle-special-character> [
4217   {
4218     comment-toggle?:bool <- equal c, 31/ctrl-slash
4219     break-unless comment-toggle?
4220     cursor-column:num <- get *editor, cursor-column:offset
4221     data:&:duplex-list:char <- get *editor, data:offset
4222     <begin-insert-character>
4223     before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
4224     line-start:&:duplex-list:char <- next before-line-start
4225     commented-out?:bool <- match line-start, [#? ]  # comment prefix
4226     {
4227       break-unless commented-out?
4228       # uncomment
4229       data <- remove line-start, 3/length-comment-prefix, data
4230       cursor-column <- subtract cursor-column, 3/length-comment-prefix
4231       *editor <- put *editor, cursor-column:offset, cursor-column
4232       go-render? <- render-line-from-start screen, editor, 3/size-of-comment-leader
4233     }
4234     {
4235       break-if commented-out?
4236       # comment
4237       insert before-line-start, [#? ]
4238       cursor-column <- add cursor-column, 3/length-comment-prefix
4239       *editor <- put *editor, cursor-column:offset, cursor-column
4240       go-render? <- render-line-from-start screen, editor, 0
4241     }
4242     <end-insert-character>
4243     return
4244   }
4245 ]
4246 
4247 # Render just from the start of the current line, and only if it wasn't
4248 # wrapping before (include margin) and isn't wrapping now. Otherwise just tell
4249 # the caller to go-render? the entire screen.
4250 def render-line-from-start screen:&:screen, editor:&:editor, right-margin:num -> go-render?:bool, screen:&:screen [
4251   local-scope
4252   load-inputs
4253   before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
4254   line-start:&:duplex-list:char <- next before-line-start
4255   color:num <- copy 7/white
4256   left:num <- get *editor, left:offset
4257   cursor-row:num <- get *editor, cursor-row:offset
4258   screen <- move-cursor screen, cursor-row, left
4259   right:num <- get *editor, right:offset
4260   end:num <- subtract right, right-margin
4261   i:num <- copy 0
4262   curr:&:duplex-list:char <- copy line-start
4263   {
4264     render-all?:bool <- greater-or-equal i, end
4265     return-if render-all?, 1/go-render
4266     break-unless curr
4267     c:char <- get *curr, value:offset
4268     newline?:bool <- equal c, 10/newline
4269     break-if newline?
4270     color <- get-color color, c
4271     print screen, c, color
4272     curr <- next curr
4273     i <- add i, 1
4274     loop
4275   }
4276   clear-line-until screen, right
4277   return 0/dont-render
4278 ]
4279 
4280 def before-start-of-screen-line editor:&:editor -> result:&:duplex-list:char [
4281   local-scope
4282   load-inputs
4283   cursor:&:duplex-list:char <- get *editor, before-cursor:offset
4284   {
4285     next:&:duplex-list:char <- next cursor
4286     break-unless next
4287     cursor <- copy next
4288   }
4289   result <- before-previous-screen-line cursor, editor
4290 ]
4291 
4292 scenario editor-comments-empty-line [
4293   local-scope
4294   assume-screen 10/width, 5/height
4295   e:&:editor <- new-editor [], 0/left, 5/right
4296   editor-render screen, e
4297   $clear-trace
4298   assume-console [
4299     press ctrl-slash
4300   ]
4301   run [
4302     editor-event-loop screen, console, e
4303     4:num/raw <- get *e, cursor-row:offset
4304     5:num/raw <- get *e, cursor-column:offset
4305   ]
4306   screen-should-contain [
4307     .          .
4308     .#?        .
4309     .╌╌╌╌╌     .
4310     .          .
4311   ]
4312   memory-should-contain [
4313     4 <- 1
4314     5 <- 3
4315   ]
4316   check-trace-count-for-label 5, [print-character]
4317 ]
4318 
4319 scenario editor-comments-at-start-of-contents [
4320   local-scope
4321   assume-screen 10/width, 5/height
4322   e:&:editor <- new-editor [ab], 0/left, 10/right
4323   editor-render screen, e
4324   $clear-trace
4325   assume-console [
4326     press ctrl-slash
4327   ]
4328   run [
4329     editor-event-loop screen, console, e
4330     4:num/raw <- get *e, cursor-row:offset
4331     5:num/raw <- get *e, cursor-column:offset
4332   ]
4333   screen-should-contain [
4334     .          .
4335     .#? ab     .
4336     .╌╌╌╌╌╌╌╌╌╌.
4337     .          .
4338   ]
4339   memory-should-contain [
4340     4 <- 1
4341     5 <- 3
4342   ]
4343   check-trace-count-for-label 10, [print-character]
4344 ]
4345 
4346 scenario editor-comments-at-end-of-contents [
4347   local-scope
4348   assume-screen 10/width, 5/height
4349   e:&:editor <- new-editor [ab], 0/left, 10/right
4350   editor-render screen, e
4351   $clear-trace
4352   assume-console [
4353     left-click 1, 7
4354     press ctrl-slash
4355   ]
4356   run [
4357     editor-event-loop screen, console, e
4358     4:num/raw <- get *e, cursor-row:offset
4359     5:num/raw <- get *e, cursor-column:offset
4360   ]
4361   screen-should-contain [
4362     .          .
4363     .#? ab     .
4364     .╌╌╌╌╌╌╌╌╌╌.
4365     .          .
4366   ]
4367   memory-should-contain [
4368     4 <- 1
4369     5 <- 5
4370   ]
4371   check-trace-count-for-label 10, [print-character]
4372   # toggle to uncomment
4373   $clear-trace
4374   assume-console [
4375     press ctrl-slash
4376   ]
4377   run [
4378     editor-event-loop screen, console, e
4379     4:num/raw <- get *e, cursor-row:offset
4380     5:num/raw <- get *e, cursor-column:offset
4381   ]
4382   screen-should-contain [
4383     .          .
4384     .ab        .
4385     .╌╌╌╌╌╌╌╌╌╌.
4386     .          .
4387   ]
4388   check-trace-count-for-label 10, [print-character]
4389 ]
4390 
4391 scenario editor-comments-almost-wrapping-line [
4392   local-scope
4393   assume-screen 10/width, 5/height
4394   # editor starts out with a non-wrapping line
4395   e:&:editor <- new-editor [abcd], 0/left, 5/right
4396   editor-render screen, e
4397   screen-should-contain [
4398     .          .
4399     .abcd      .
4400     .╌╌╌╌╌     .
4401     .          .
4402   ]
4403   $clear-trace
4404   # on commenting the line is now wrapped
4405   assume-console [
4406     left-click 1, 7
4407     press ctrl-slash
4408   ]
4409   run [
4410     editor-event-loop screen, console, e
4411   ]
4412   screen-should-contain [
4413     .          .
4414     .#? a↩     .
4415     .bcd       .
4416     .╌╌╌╌╌     .
4417     .          .
4418   ]
4419 ]
4420 
4421 scenario editor-uncomments-just-wrapping-line [
4422   local-scope
4423   assume-screen 10/width, 5/height
4424   # editor starts out with a comment that wraps the line
4425   e:&:editor <- new-editor [#? ab], 0/left, 5/right
4426   editor-render screen, e
4427   screen-should-contain [
4428     .          .
4429     .#? a↩     .
4430     .b         .
4431     .╌╌╌╌╌     .
4432     .          .
4433   ]
4434   $clear-trace
4435   # on uncommenting the line is no longer wrapped
4436   assume-console [
4437     left-click 1, 7
4438     press ctrl-slash
4439   ]
4440   run [
4441     editor-event-loop screen, console, e
4442   ]
4443   screen-should-contain [
4444     .          .
4445     .ab        .
4446     .╌╌╌╌╌     .
4447     .          .
4448   ]
4449 ]