https://github.com/akkartik/mu1/blob/master/edit/003-shortcuts.mu
   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, false/no-more-render, null/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?, true/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?, true/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?, true/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 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 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 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, false/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?, true/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?, true/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 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?, false/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 true/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?, false/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 true/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 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, false/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 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 true/go-render
1060   }
1061 ]
1062 
1063 # Takes a pointer into the doubly-linked list, scans back to before start of
1064 # previous *wrapped* line.
1065 # Returns original if no next newline.
1066 # Beware: never return null pointer.
1067 def before-previous-screen-line in:&:duplex-list:char, editor:&:editor -> out:&:duplex-list:char [
1068   local-scope
1069   load-inputs
1070   curr:&:duplex-list:char <- copy in
1071   c:char <- get *curr, value:offset
1072   # compute max, number of characters to skip
1073   #   1 + len%(width-1)
1074   #   except rotate second term to vary from 1 to width-1 rather than 0 to width-2
1075   left:num <- get *editor, left:offset
1076   right:num <- get *editor, right:offset
1077   max-line-length:num <- subtract right, left, -1/exclusive-right, 1/wrap-icon
1078   sentinel:&:duplex-list:char <- get *editor, data:offset
1079   len:num <- previous-line-length curr, sentinel
1080   {
1081     break-if len
1082     # empty line; just skip this newline
1083     prev:&:duplex-list:char <- prev curr
1084     return-unless prev, curr
1085     return prev
1086   }
1087   _, max:num <- divide-with-remainder len, max-line-length
1088   # remainder 0 => scan one width-worth
1089   {
1090     break-if max
1091     max <- copy max-line-length
1092   }
1093   max <- add max, 1
1094   count:num <- copy 0
1095   # skip 'max' characters
1096   {
1097     done?:bool <- greater-or-equal count, max
1098     break-if done?
1099     prev:&:duplex-list:char <- prev curr
1100     break-unless prev
1101     curr <- copy prev
1102     count <- add count, 1
1103     loop
1104   }
1105   return curr
1106 ]
1107 
1108 scenario editor-adjusts-column-at-previous-line [
1109   local-scope
1110   assume-screen 10/width, 5/height
1111   s:text <- new [ab
1112 def]
1113   e:&:editor <- new-editor s, 0/left, 10/right
1114   editor-render screen, e
1115   $clear-trace
1116   assume-console [
1117     left-click 2, 3
1118     press up-arrow
1119   ]
1120   run [
1121     editor-event-loop screen, console, e
1122     3:num/raw <- get *e, cursor-row:offset
1123     4:num/raw <- get *e, cursor-column:offset
1124   ]
1125   memory-should-contain [
1126     3 <- 1
1127     4 <- 2
1128   ]
1129   check-trace-count-for-label 0, [print-character]
1130   assume-console [
1131     type [0]
1132   ]
1133   run [
1134     editor-event-loop screen, console, e
1135   ]
1136   screen-should-contain [
1137     .          .
1138     .ab0       .
1139     .def       .
1140     .╌╌╌╌╌╌╌╌╌╌.
1141     .          .
1142   ]
1143 ]
1144 
1145 scenario editor-adjusts-column-at-empty-line [
1146   local-scope
1147   assume-screen 10/width, 5/height
1148   s:text <- new [
1149 def]
1150   e:&:editor <- new-editor s, 0/left, 10/right
1151   editor-render screen, e
1152   $clear-trace
1153   assume-console [
1154     left-click 2, 3
1155     press up-arrow
1156   ]
1157   run [
1158     editor-event-loop screen, console, e
1159     3:num/raw <- get *e, cursor-row:offset
1160     4:num/raw <- get *e, cursor-column:offset
1161   ]
1162   memory-should-contain [
1163     3 <- 1
1164     4 <- 0
1165   ]
1166   check-trace-count-for-label 0, [print-character]
1167   assume-console [
1168     type [0]
1169   ]
1170   run [
1171     editor-event-loop screen, console, e
1172   ]
1173   screen-should-contain [
1174     .          .
1175     .0         .
1176     .def       .
1177     .╌╌╌╌╌╌╌╌╌╌.
1178     .          .
1179   ]
1180 ]
1181 
1182 scenario editor-moves-to-previous-line-from-zero-margin [
1183   local-scope
1184   assume-screen 10/width, 5/height
1185   # start out with three lines
1186   s:text <- new [abc
1187 def
1188 ghi]
1189   e:&:editor <- new-editor s, 0/left, 10/right
1190   editor-render screen, e
1191   $clear-trace
1192   # click on the third line and hit up-arrow, so you end up just after a newline
1193   assume-console [
1194     left-click 3, 0
1195     press up-arrow
1196   ]
1197   run [
1198     editor-event-loop screen, console, e
1199     3:num/raw <- get *e, cursor-row:offset
1200     4:num/raw <- get *e, cursor-column:offset
1201   ]
1202   memory-should-contain [
1203     3 <- 2
1204     4 <- 0
1205   ]
1206   check-trace-count-for-label 0, [print-character]
1207   assume-console [
1208     type [0]
1209   ]
1210   run [
1211     editor-event-loop screen, console, e
1212   ]
1213   screen-should-contain [
1214     .          .
1215     .abc       .
1216     .0def      .
1217     .ghi       .
1218     .╌╌╌╌╌╌╌╌╌╌.
1219   ]
1220 ]
1221 
1222 scenario editor-moves-to-previous-line-from-left-margin [
1223   local-scope
1224   assume-screen 10/width, 5/height
1225   # start out with three lines
1226   s:text <- new [abc
1227 def
1228 ghi]
1229   e:&:editor <- new-editor s, 1/left, 10/right
1230   editor-render screen, e
1231   $clear-trace
1232   # click on the third line and hit up-arrow, so you end up just after a newline
1233   assume-console [
1234     left-click 3, 1
1235     press up-arrow
1236   ]
1237   run [
1238     editor-event-loop screen, console, e
1239     3:num/raw <- get *e, cursor-row:offset
1240     4:num/raw <- get *e, cursor-column:offset
1241   ]
1242   memory-should-contain [
1243     3 <- 2
1244     4 <- 1
1245   ]
1246   check-trace-count-for-label 0, [print-character]
1247   assume-console [
1248     type [0]
1249   ]
1250   run [
1251     editor-event-loop screen, console, e
1252   ]
1253   screen-should-contain [
1254     .          .
1255     . abc      .
1256     . 0def     .
1257     . ghi      .
1258     . ╌╌╌╌╌╌╌╌╌.
1259   ]
1260 ]
1261 
1262 scenario editor-moves-to-top-line-in-presence-of-wrapped-line [
1263   local-scope
1264   assume-screen 10/width, 5/height
1265   e:&:editor <- new-editor [abcde], 0/left, 5/right
1266   editor-render screen, e
1267   screen-should-contain [
1268     .          .
1269     .abcd↩     .
1270     .e         .
1271     .╌╌╌╌╌     .
1272   ]
1273   $clear-trace
1274   assume-console [
1275     left-click 2, 0
1276     press up-arrow
1277   ]
1278   run [
1279     editor-event-loop screen, console, e
1280     3:num/raw <- get *e, cursor-row:offset
1281     4:num/raw <- get *e, cursor-column:offset
1282   ]
1283   memory-should-contain [
1284     3 <- 1
1285     4 <- 0
1286   ]
1287   check-trace-count-for-label 0, [print-character]
1288   assume-console [
1289     type [0]
1290   ]
1291   run [
1292     editor-event-loop screen, console, e
1293   ]
1294   screen-should-contain [
1295     .          .
1296     .0abc↩     .
1297     .de        .
1298     .╌╌╌╌╌     .
1299   ]
1300 ]
1301 
1302 scenario editor-moves-to-top-line-in-presence-of-wrapped-line-2 [
1303   local-scope
1304   assume-screen 10/width, 5/height
1305   s:text <- new [abc
1306 defgh]
1307   e:&:editor <- new-editor s, 0/left, 5/right
1308   editor-render screen, e
1309   screen-should-contain [
1310     .          .
1311     .abc       .
1312     .defg↩     .
1313     .h         .
1314     .╌╌╌╌╌     .
1315   ]
1316   $clear-trace
1317   assume-console [
1318     left-click 3, 0
1319     press up-arrow
1320     press up-arrow
1321   ]
1322   run [
1323     editor-event-loop screen, console, e
1324     3:num/raw <- get *e, cursor-row:offset
1325     4:num/raw <- get *e, cursor-column:offset
1326   ]
1327   memory-should-contain [
1328     3 <- 1
1329     4 <- 0
1330   ]
1331   check-trace-count-for-label 0, [print-character]
1332   assume-console [
1333     type [0]
1334   ]
1335   run [
1336     editor-event-loop screen, console, e
1337   ]
1338   screen-should-contain [
1339     .          .
1340     .0abc      .
1341     .defg↩     .
1342     .h         .
1343     .╌╌╌╌╌     .
1344   ]
1345 ]
1346 
1347 # down arrow
1348 
1349 scenario editor-moves-to-next-line-with-down-arrow [
1350   local-scope
1351   assume-screen 10/width, 5/height
1352   s:text <- new [abc
1353 def]
1354   e:&:editor <- new-editor s, 0/left, 10/right
1355   editor-render screen, e
1356   $clear-trace
1357   # cursor starts out at (1, 0)
1358   assume-console [
1359     press down-arrow
1360   ]
1361   run [
1362     editor-event-loop screen, console, e
1363     3:num/raw <- get *e, cursor-row:offset
1364     4:num/raw <- get *e, cursor-column:offset
1365   ]
1366   # ..and ends at (2, 0)
1367   memory-should-contain [
1368     3 <- 2
1369     4 <- 0
1370   ]
1371   check-trace-count-for-label 0, [print-character]
1372   assume-console [
1373     type [0]
1374   ]
1375   run [
1376     editor-event-loop screen, console, e
1377   ]
1378   screen-should-contain [
1379     .          .
1380     .abc       .
1381     .0def      .
1382     .╌╌╌╌╌╌╌╌╌╌.
1383     .          .
1384   ]
1385 ]
1386 
1387 after <handle-special-key> [
1388   {
1389     move-to-next-line?:bool <- equal k, 65516/down-arrow
1390     break-unless move-to-next-line?
1391     <begin-move-cursor>
1392     go-render? <- move-to-next-line editor, screen-height
1393     undo-coalesce-tag:num <- copy 4/down-arrow
1394     <end-move-cursor>
1395     return
1396   }
1397 ]
1398 
1399 def move-to-next-line editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
1400   local-scope
1401   load-inputs
1402   cursor-row:num <- get *editor, cursor-row:offset
1403   cursor-column:num <- get *editor, cursor-column:offset
1404   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1405   left:num <- get *editor, left:offset
1406   right:num <- get *editor, right:offset
1407   last-line:num <- subtract screen-height, 1
1408   bottom:num <- get *editor, bottom:offset
1409   at-bottom-of-screen?:bool <- greater-or-equal bottom, last-line
1410   {
1411     break-if before-cursor
1412     {
1413       break-if at-bottom-of-screen?
1414       return false/don't-render
1415     }
1416     {
1417       break-unless at-bottom-of-screen?
1418       jump +try-to-scroll
1419     }
1420   }
1421   next:&:duplex-list:char <- next before-cursor
1422   {
1423     break-if next
1424     {
1425       break-if at-bottom-of-screen?
1426       return false/don't-render
1427     }
1428     {
1429       break-unless at-bottom-of-screen?
1430       jump +try-to-scroll
1431     }
1432   }
1433   already-at-bottom?:bool <- greater-or-equal cursor-row, last-line
1434   {
1435     # if cursor not at bottom, move it
1436     break-if already-at-bottom?
1437     target-column:num <- copy cursor-column
1438     # scan to start of next line
1439     {
1440       next:&:duplex-list:char <- next before-cursor
1441       break-unless next
1442       done?:bool <- greater-or-equal cursor-column, right
1443       break-if done?
1444       cursor-column <- add cursor-column, 1
1445       before-cursor <- copy next
1446       c:char <- get *next, value:offset
1447       at-newline?:bool <- equal c, 10/newline
1448       break-if at-newline?
1449       loop
1450     }
1451     {
1452       break-if next
1453       {
1454         break-if at-bottom-of-screen?
1455         return false/don't-render
1456       }
1457       {
1458         break-unless at-bottom-of-screen?
1459         jump +try-to-scroll
1460       }
1461     }
1462     cursor-row <- add cursor-row, 1
1463     cursor-column <- copy left
1464     {
1465       next:&:duplex-list:char <- next before-cursor
1466       break-unless next
1467       c:char <- get *next, value:offset
1468       at-newline?:bool <- equal c, 10/newline
1469       break-if at-newline?
1470       done?:bool <- greater-or-equal cursor-column, target-column
1471       break-if done?
1472       cursor-column <- add cursor-column, 1
1473       before-cursor <- copy next
1474       loop
1475     }
1476     *editor <- put *editor, before-cursor:offset, before-cursor
1477     *editor <- put *editor, cursor-column:offset, cursor-column
1478     *editor <- put *editor, cursor-row:offset, cursor-row
1479     return false/don't-render
1480   }
1481   +try-to-scroll
1482   <scroll-down>
1483   go-render? <- copy true
1484 ]
1485 
1486 scenario editor-adjusts-column-at-next-line [
1487   local-scope
1488   assume-screen 10/width, 5/height
1489   # second line is shorter than first
1490   s:text <- new [abcde
1491 fg
1492 hi]
1493   e:&:editor <- new-editor s, 0/left, 10/right
1494   editor-render screen, e
1495   $clear-trace
1496   # move to end of first line, then press down
1497   assume-console [
1498     left-click 1, 8
1499     press down-arrow
1500   ]
1501   run [
1502     editor-event-loop screen, console, e
1503     3:num/raw <- get *e, cursor-row:offset
1504     4:num/raw <- get *e, cursor-column:offset
1505   ]
1506   # cursor doesn't go vertically down, it goes to end of shorter line
1507   memory-should-contain [
1508     3 <- 2
1509     4 <- 2
1510   ]
1511   check-trace-count-for-label 0, [print-character]
1512   assume-console [
1513     type [0]
1514   ]
1515   run [
1516     editor-event-loop screen, console, e
1517   ]
1518   screen-should-contain [
1519     .          .
1520     .abcde     .
1521     .fg0       .
1522     .hi        .
1523     .╌╌╌╌╌╌╌╌╌╌.
1524   ]
1525 ]
1526 
1527 scenario editor-moves-down-within-wrapped-line [
1528   local-scope
1529   assume-screen 10/width, 5/height
1530   e:&:editor <- new-editor [abcdefghijklmno], 0/left, 10/right
1531   editor-render screen, e
1532   screen-should-contain [
1533     .          .
1534     .abcdefghi↩.
1535     .jklmno    .
1536     .╌╌╌╌╌╌╌╌╌╌.
1537     .          .
1538   ]
1539   # position cursor on first screen line, but past end of second screen line
1540   assume-console [
1541     left-click 1, 8
1542     press down-arrow
1543   ]
1544   run [
1545     editor-event-loop screen, console, e
1546     3:num/raw <- get *e, cursor-row:offset
1547     4:num/raw <- get *e, cursor-column:offset
1548   ]
1549   # cursor should be at end of second screen line
1550   memory-should-contain [
1551     3 <- 2
1552     4 <- 6
1553   ]
1554 ]
1555 
1556 # ctrl-a/home - move cursor to start of line
1557 
1558 scenario editor-moves-to-start-of-line-with-ctrl-a [
1559   local-scope
1560   assume-screen 10/width, 5/height
1561   s:text <- new [123
1562 456]
1563   e:&:editor <- new-editor s, 0/left, 10/right
1564   editor-render screen, e
1565   $clear-trace
1566   # start on second line, press ctrl-a
1567   assume-console [
1568     left-click 2, 3
1569     press ctrl-a
1570   ]
1571   run [
1572     editor-event-loop screen, console, e
1573     4:num/raw <- get *e, cursor-row:offset
1574     5:num/raw <- get *e, cursor-column:offset
1575   ]
1576   # cursor moves to start of line
1577   memory-should-contain [
1578     4 <- 2
1579     5 <- 0
1580   ]
1581   check-trace-count-for-label 0, [print-character]
1582 ]
1583 
1584 after <handle-special-character> [
1585   {
1586     move-to-start-of-line?:bool <- equal c, 1/ctrl-a
1587     break-unless move-to-start-of-line?
1588     <begin-move-cursor>
1589     move-to-start-of-screen-line editor
1590     undo-coalesce-tag:num <- copy 0/never
1591     <end-move-cursor>
1592     return false/don't-render
1593   }
1594 ]
1595 
1596 after <handle-special-key> [
1597   {
1598     move-to-start-of-line?:bool <- equal k, 65521/home
1599     break-unless move-to-start-of-line?
1600     <begin-move-cursor>
1601     move-to-start-of-screen-line editor
1602     undo-coalesce-tag:num <- copy 0/never
1603     <end-move-cursor>
1604     return false/don't-render
1605   }
1606 ]
1607 
1608 # handles wrapped lines
1609 # precondition: cursor-column should be in a consistent state
1610 def move-to-start-of-screen-line editor:&:editor -> editor:&:editor [
1611   local-scope
1612   load-inputs
1613   # update cursor column
1614   left:num <- get *editor, left:offset
1615   col:num <- get *editor, cursor-column:offset
1616   # update before-cursor
1617   curr:&:duplex-list:char <- get *editor, before-cursor:offset
1618   # while not at start of line, move
1619   {
1620     done?:bool <- equal col, left
1621     break-if done?
1622     assert curr, [move-to-start-of-line tried to move before start of text]
1623     curr <- prev curr
1624     col <- subtract col, 1
1625     loop
1626   }
1627   *editor <- put *editor, cursor-column:offset, col
1628   *editor <- put *editor, before-cursor:offset, curr
1629 ]
1630 
1631 scenario editor-moves-to-start-of-line-with-ctrl-a-2 [
1632   local-scope
1633   assume-screen 10/width, 5/height
1634   s:text <- new [123
1635 456]
1636   e:&:editor <- new-editor s, 0/left, 10/right
1637   editor-render screen, e
1638   $clear-trace
1639   # start on first line (no newline before), press ctrl-a
1640   assume-console [
1641     left-click 1, 3
1642     press ctrl-a
1643   ]
1644   run [
1645     editor-event-loop screen, console, e
1646     4:num/raw <- get *e, cursor-row:offset
1647     5:num/raw <- get *e, cursor-column:offset
1648   ]
1649   # cursor moves to start of line
1650   memory-should-contain [
1651     4 <- 1
1652     5 <- 0
1653   ]
1654   check-trace-count-for-label 0, [print-character]
1655 ]
1656 
1657 scenario editor-moves-to-start-of-line-with-home [
1658   local-scope
1659   assume-screen 10/width, 5/height
1660   s:text <- new [123
1661 456]
1662   e:&:editor <- new-editor s, 0/left, 10/right
1663   $clear-trace
1664   # start on second line, press 'home'
1665   assume-console [
1666     left-click 2, 3
1667     press home
1668   ]
1669   run [
1670     editor-event-loop screen, console, e
1671     3:num/raw <- get *e, cursor-row:offset
1672     4:num/raw <- get *e, cursor-column:offset
1673   ]
1674   # cursor moves to start of line
1675   memory-should-contain [
1676     3 <- 2
1677     4 <- 0
1678   ]
1679   check-trace-count-for-label 0, [print-character]
1680 ]
1681 
1682 scenario editor-moves-to-start-of-line-with-home-2 [
1683   local-scope
1684   assume-screen 10/width, 5/height
1685   s:text <- new [123
1686 456]
1687   e:&:editor <- new-editor s, 0/left, 10/right
1688   editor-render screen, e
1689   $clear-trace
1690   # start on first line (no newline before), press 'home'
1691   assume-console [
1692     left-click 1, 3
1693     press home
1694   ]
1695   run [
1696     editor-event-loop screen, console, e
1697     3:num/raw <- get *e, cursor-row:offset
1698     4:num/raw <- get *e, cursor-column:offset
1699   ]
1700   # cursor moves to start of line
1701   memory-should-contain [
1702     3 <- 1
1703     4 <- 0
1704   ]
1705   check-trace-count-for-label 0, [print-character]
1706 ]
1707 
1708 scenario editor-moves-to-start-of-screen-line-with-ctrl-a [
1709   local-scope
1710   assume-screen 10/width, 5/height
1711   e:&:editor <- new-editor [123456], 0/left, 5/right
1712   editor-render screen, e
1713   screen-should-contain [
1714     .          .
1715     .1234↩     .
1716     .56        .
1717     .╌╌╌╌╌     .
1718     .          .
1719   ]
1720   $clear-trace
1721   # start on second line, press ctrl-a then up
1722   assume-console [
1723     left-click 2, 1
1724     press ctrl-a
1725     press up-arrow
1726   ]
1727   run [
1728     editor-event-loop screen, console, e
1729     4:num/raw <- get *e, cursor-row:offset
1730     5:num/raw <- get *e, cursor-column:offset
1731   ]
1732   # cursor moves to start of first line
1733   memory-should-contain [
1734     4 <- 1  # cursor-row
1735     5 <- 0  # cursor-column
1736   ]
1737   check-trace-count-for-label 0, [print-character]
1738   # make sure before-cursor is in sync
1739   assume-console [
1740     type [a]
1741   ]
1742   run [
1743     editor-event-loop screen, console, e
1744     4:num/raw <- get *e, cursor-row:offset
1745     5:num/raw <- get *e, cursor-column:offset
1746   ]
1747   screen-should-contain [
1748     .          .
1749     .a123↩     .
1750     .456       .
1751     .╌╌╌╌╌     .
1752     .          .
1753   ]
1754   memory-should-contain [
1755     4 <- 1  # cursor-row
1756     5 <- 1  # cursor-column
1757   ]
1758 ]
1759 
1760 # ctrl-e/end - move cursor to end of line
1761 
1762 scenario editor-moves-to-end-of-line-with-ctrl-e [
1763   local-scope
1764   assume-screen 10/width, 5/height
1765   s:text <- new [123
1766 456]
1767   e:&:editor <- new-editor s, 0/left, 10/right
1768   editor-render screen, e
1769   $clear-trace
1770   # start on first line, press ctrl-e
1771   assume-console [
1772     left-click 1, 1
1773     press ctrl-e
1774   ]
1775   run [
1776     editor-event-loop screen, console, e
1777     4:num/raw <- get *e, cursor-row:offset
1778     5:num/raw <- get *e, cursor-column:offset
1779   ]
1780   # cursor moves to end of line
1781   memory-should-contain [
1782     4 <- 1
1783     5 <- 3
1784   ]
1785   check-trace-count-for-label 0, [print-character]
1786   # editor inserts future characters at cursor
1787   assume-console [
1788     type [z]
1789   ]
1790   run [
1791     editor-event-loop screen, console, e
1792     4:num/raw <- get *e, cursor-row:offset
1793     5:num/raw <- get *e, cursor-column:offset
1794   ]
1795   memory-should-contain [
1796     4 <- 1
1797     5 <- 4
1798   ]
1799   screen-should-contain [
1800     .          .
1801     .123z      .
1802     .456       .
1803     .╌╌╌╌╌╌╌╌╌╌.
1804     .          .
1805   ]
1806   check-trace-count-for-label 1, [print-character]
1807 ]
1808 
1809 after <handle-special-character> [
1810   {
1811     move-to-end-of-line?:bool <- equal c, 5/ctrl-e
1812     break-unless move-to-end-of-line?
1813     <begin-move-cursor>
1814     move-to-end-of-line editor
1815     undo-coalesce-tag:num <- copy 0/never
1816     <end-move-cursor>
1817     return false/don't-render
1818   }
1819 ]
1820 
1821 after <handle-special-key> [
1822   {
1823     move-to-end-of-line?:bool <- equal k, 65520/end
1824     break-unless move-to-end-of-line?
1825     <begin-move-cursor>
1826     move-to-end-of-line editor
1827     undo-coalesce-tag:num <- copy 0/never
1828     <end-move-cursor>
1829     return false/don't-render
1830   }
1831 ]
1832 
1833 def move-to-end-of-line editor:&:editor -> editor:&:editor [
1834   local-scope
1835   load-inputs
1836   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1837   cursor-column:num <- get *editor, cursor-column:offset
1838   right:num <- get *editor, right:offset
1839   # while not at end of line, move
1840   {
1841     next:&:duplex-list:char <- next before-cursor
1842     break-unless next  # end of text
1843     nextc:char <- get *next, value:offset
1844     at-end-of-line?:bool <- equal nextc, 10/newline
1845     break-if at-end-of-line?
1846     cursor-column <- add cursor-column, 1
1847     at-right?:bool <- equal cursor-column, right
1848     break-if at-right?
1849     *editor <- put *editor, cursor-column:offset, cursor-column
1850     before-cursor <- copy next
1851     *editor <- put *editor, before-cursor:offset, before-cursor
1852     loop
1853   }
1854 ]
1855 
1856 scenario editor-moves-to-end-of-line-with-ctrl-e-2 [
1857   local-scope
1858   assume-screen 10/width, 5/height
1859   s:text <- new [123
1860 456]
1861   e:&:editor <- new-editor s, 0/left, 10/right
1862   editor-render screen, e
1863   $clear-trace
1864   # start on second line (no newline after), press ctrl-e
1865   assume-console [
1866     left-click 2, 1
1867     press ctrl-e
1868   ]
1869   run [
1870     editor-event-loop screen, console, e
1871     4:num/raw <- get *e, cursor-row:offset
1872     5:num/raw <- get *e, cursor-column:offset
1873   ]
1874   # cursor moves to end of line
1875   memory-should-contain [
1876     4 <- 2
1877     5 <- 3
1878   ]
1879   check-trace-count-for-label 0, [print-character]
1880 ]
1881 
1882 scenario editor-moves-to-end-of-line-with-end [
1883   local-scope
1884   assume-screen 10/width, 5/height
1885   s:text <- new [123
1886 456]
1887   e:&:editor <- new-editor s, 0/left, 10/right
1888   editor-render screen, e
1889   $clear-trace
1890   # start on first line, press 'end'
1891   assume-console [
1892     left-click 1, 1
1893     press end
1894   ]
1895   run [
1896     editor-event-loop screen, console, e
1897     3:num/raw <- get *e, cursor-row:offset
1898     4:num/raw <- get *e, cursor-column:offset
1899   ]
1900   # cursor moves to end of line
1901   memory-should-contain [
1902     3 <- 1
1903     4 <- 3
1904   ]
1905   check-trace-count-for-label 0, [print-character]
1906 ]
1907 
1908 scenario editor-moves-to-end-of-line-with-end-2 [
1909   local-scope
1910   assume-screen 10/width, 5/height
1911   s:text <- new [123
1912 456]
1913   e:&:editor <- new-editor s, 0/left, 10/right
1914   editor-render screen, e
1915   $clear-trace
1916   # start on second line (no newline after), press 'end'
1917   assume-console [
1918     left-click 2, 1
1919     press end
1920   ]
1921   run [
1922     editor-event-loop screen, console, e
1923     3:num/raw <- get *e, cursor-row:offset
1924     4:num/raw <- get *e, cursor-column:offset
1925   ]
1926   # cursor moves to end of line
1927   memory-should-contain [
1928     3 <- 2
1929     4 <- 3
1930   ]
1931   check-trace-count-for-label 0, [print-character]
1932 ]
1933 
1934 scenario editor-moves-to-end-of-wrapped-line [
1935   local-scope
1936   assume-screen 10/width, 5/height
1937   s:text <- new [123456
1938 789]
1939   e:&:editor <- new-editor s, 0/left, 5/right
1940   editor-render screen, e
1941   $clear-trace
1942   # start on first line, press 'end'
1943   assume-console [
1944     left-click 1, 1
1945     press end
1946   ]
1947   run [
1948     editor-event-loop screen, console, e
1949     10:num/raw <- get *e, cursor-row:offset
1950     11:num/raw <- get *e, cursor-column:offset
1951   ]
1952   # cursor moves to end of line
1953   memory-should-contain [
1954     10 <- 1
1955     11 <- 3
1956   ]
1957   # no prints
1958   check-trace-count-for-label 0, [print-character]
1959   # before-cursor is also consistent
1960   assume-console [
1961     type [a]
1962   ]
1963   run [
1964     editor-event-loop screen, console, e
1965   ]
1966   screen-should-contain [
1967     .          .
1968     .123a↩     .
1969     .456       .
1970     .789       .
1971     .╌╌╌╌╌     .
1972   ]
1973 ]
1974 
1975 # ctrl-u - delete text from start of line until (but not at) cursor
1976 
1977 scenario editor-deletes-to-start-of-line-with-ctrl-u [
1978   local-scope
1979   assume-screen 10/width, 5/height
1980   s:text <- new [123
1981 456]
1982   e:&:editor <- new-editor s, 0/left, 10/right
1983   editor-render screen, e
1984   $clear-trace
1985   # start on second line, press ctrl-u
1986   assume-console [
1987     left-click 2, 2
1988     press ctrl-u
1989   ]
1990   run [
1991     editor-event-loop screen, console, e
1992   ]
1993   # cursor deletes to start of line
1994   screen-should-contain [
1995     .          .
1996     .123       .
1997     .6         .
1998     .╌╌╌╌╌╌╌╌╌╌.
1999     .          .
2000   ]
2001   check-trace-count-for-label 10, [print-character]
2002 ]
2003 
2004 after <handle-special-character> [
2005   {
2006     delete-to-start-of-line?:bool <- equal c, 21/ctrl-u
2007     break-unless delete-to-start-of-line?
2008     <begin-delete-to-start-of-line>
2009     deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor
2010     <end-delete-to-start-of-line>
2011     go-render?:bool <- minimal-render-for-ctrl-u screen, editor, deleted-cells
2012     return
2013   }
2014 ]
2015 
2016 def minimal-render-for-ctrl-u screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
2017   local-scope
2018   load-inputs
2019   curr-column:num <- get *editor, cursor-column:offset
2020   # accumulate the current line as text and render it
2021   buf:&:buffer:char <- new-buffer 30  # accumulator for the text we need to render
2022   curr:&:duplex-list:char <- get *editor, before-cursor:offset
2023   i:num <- copy curr-column
2024   right:num <- get *editor, right:offset
2025   {
2026     # if we have a wrapped line, give up and render the whole screen
2027     wrap?:bool <- greater-or-equal i, right
2028     return-if wrap?, true/go-render
2029     curr <- next curr
2030     break-unless curr
2031     c:char <- get *curr, value:offset
2032     b:bool <- equal c, 10
2033     break-if b
2034     buf <- append buf, c
2035     i <- add i, 1
2036     loop
2037   }
2038   # if the line used to be wrapped, give up and render the whole screen
2039   num-deleted-cells:num <- length deleted-cells
2040   old-row-len:num <- add i, num-deleted-cells
2041   left:num <- get *editor, left:offset
2042   end:num <- subtract right, left
2043   wrap?:bool <- greater-or-equal old-row-len, end
2044   return-if wrap?, true/go-render
2045   curr-line:text <- buffer-to-array buf
2046   curr-row:num <- get *editor, cursor-row:offset
2047   render-code screen, curr-line, curr-column, right, curr-row
2048   return false/dont-render
2049 ]
2050 
2051 def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
2052   local-scope
2053   load-inputs
2054   # compute range to delete
2055   init:&:duplex-list:char <- get *editor, data:offset
2056   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2057   update-top-of-screen?:bool <- copy false
2058   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
2059   start:&:duplex-list:char <- copy before-cursor
2060   end:&:duplex-list:char <- next before-cursor
2061   {
2062     at-start-of-text?:bool <- equal start, init
2063     break-if at-start-of-text?
2064     curr:char <- get *start, value:offset
2065     at-start-of-line?:bool <- equal curr, 10/newline
2066     break-if at-start-of-line?
2067     # if we went past top-of-screen, make a note to update it as well
2068     at-top-of-screen?:bool <- equal start, top-of-screen
2069     update-top-of-screen?:bool <- or update-top-of-screen?, at-top-of-screen?
2070     start <- prev start
2071     assert start, [delete-to-start-of-line tried to move before start of text]
2072     loop
2073   }
2074   # snip it out
2075   result:&:duplex-list:char <- next start
2076   remove-between start, end
2077   # update top-of-screen if it's just been invalidated
2078   {
2079     break-unless update-top-of-screen?
2080     put *editor, top-of-screen:offset, start
2081   }
2082   # adjust cursor
2083   before-cursor <- copy start
2084   *editor <- put *editor, before-cursor:offset, before-cursor
2085   left:num <- get *editor, left:offset
2086   *editor <- put *editor, cursor-column:offset, left
2087   # if the line wrapped before, we may need to adjust cursor-row as well
2088   right:num <- get *editor, right:offset
2089   width:num <- subtract right, left
2090   num-deleted:num <- length result
2091   cursor-row-adjustment:num <- divide-with-remainder num-deleted, width
2092   return-unless cursor-row-adjustment
2093   cursor-row:num <- get *editor, cursor-row:offset
2094   cursor-row-in-editor:num <- subtract cursor-row, 1  # ignore menubar
2095   at-top?:bool <- lesser-or-equal cursor-row-in-editor, cursor-row-adjustment
2096   {
2097     break-unless at-top?
2098     cursor-row <- copy 1  # top of editor, below menubar
2099   }
2100   {
2101     break-if at-top?
2102     cursor-row <- subtract cursor-row, cursor-row-adjustment
2103   }
2104   put *editor, cursor-row:offset, cursor-row
2105 ]
2106 
2107 def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [
2108   local-scope
2109   load-inputs
2110   return-unless s
2111   color:num <- copy 7/white
2112   column:num <- copy left
2113   screen <- move-cursor screen, row, column
2114   screen-height:num <- screen-height screen
2115   i:num <- copy 0
2116   len:num <- length *s
2117   {
2118     +next-character
2119     done?:bool <- greater-or-equal i, len
2120     break-if done?
2121     done? <- greater-or-equal row, screen-height
2122     break-if done?
2123     c:char <- index *s, i
2124     <character-c-received>
2125     {
2126       # newline? move to left rather than 0
2127       newline?:bool <- equal c, 10/newline
2128       break-unless newline?
2129       # clear rest of line in this window
2130       {
2131         done?:bool <- greater-than column, right
2132         break-if done?
2133         space:char <- copy 32/space
2134         print screen, space
2135         column <- add column, 1
2136         loop
2137       }
2138       row <- add row, 1
2139       column <- copy left
2140       screen <- move-cursor screen, row, column
2141       i <- add i, 1
2142       loop +next-character
2143     }
2144     {
2145       # at right? wrap.
2146       at-right?:bool <- equal column, right
2147       break-unless at-right?
2148       # print wrap icon
2149       wrap-icon:char <- copy 8617/loop-back-to-left
2150       print screen, wrap-icon, 245/grey
2151       column <- copy left
2152       row <- add row, 1
2153       screen <- move-cursor screen, row, column
2154       # don't increment i
2155       loop +next-character
2156     }
2157     i <- add i, 1
2158     print screen, c, color
2159     column <- add column, 1
2160     loop
2161   }
2162   was-at-left?:bool <- equal column, left
2163   clear-line-until screen, right
2164   {
2165     break-if was-at-left?
2166     row <- add row, 1
2167   }
2168   move-cursor screen, row, left
2169 ]
2170 
2171 scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
2172   local-scope
2173   assume-screen 10/width, 5/height
2174   s:text <- new [123
2175 456]
2176   e:&:editor <- new-editor s, 0/left, 10/right
2177   editor-render screen, e
2178   $clear-trace
2179   # start on first line (no newline before), press ctrl-u
2180   assume-console [
2181     left-click 1, 2
2182     press ctrl-u
2183   ]
2184   run [
2185     editor-event-loop screen, console, e
2186   ]
2187   # cursor deletes to start of line
2188   screen-should-contain [
2189     .          .
2190     .3         .
2191     .456       .
2192     .╌╌╌╌╌╌╌╌╌╌.
2193     .          .
2194   ]
2195   check-trace-count-for-label 10, [print-character]
2196 ]
2197 
2198 scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
2199   local-scope
2200   assume-screen 10/width, 5/height
2201   s:text <- new [123
2202 456]
2203   e:&:editor <- new-editor s, 0/left, 10/right
2204   editor-render screen, e
2205   $clear-trace
2206   # start past end of line, press ctrl-u
2207   assume-console [
2208     left-click 1, 3
2209     press ctrl-u
2210   ]
2211   run [
2212     editor-event-loop screen, console, e
2213   ]
2214   # cursor deletes to start of line
2215   screen-should-contain [
2216     .          .
2217     .          .
2218     .456       .
2219     .╌╌╌╌╌╌╌╌╌╌.
2220     .          .
2221   ]
2222   check-trace-count-for-label 10, [print-character]
2223 ]
2224 
2225 scenario editor-deletes-to-start-of-final-line-with-ctrl-u [
2226   local-scope
2227   assume-screen 10/width, 5/height
2228   s:text <- new [123
2229 456]
2230   e:&:editor <- new-editor s, 0/left, 10/right
2231   editor-render screen, e
2232   $clear-trace
2233   # start past end of final line, press ctrl-u
2234   assume-console [
2235     left-click 2, 3
2236     press ctrl-u
2237   ]
2238   run [
2239     editor-event-loop screen, console, e
2240   ]
2241   # cursor deletes to start of line
2242   screen-should-contain [
2243     .          .
2244     .123       .
2245     .          .
2246     .╌╌╌╌╌╌╌╌╌╌.
2247     .          .
2248   ]
2249   check-trace-count-for-label 10, [print-character]
2250 ]
2251 
2252 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u [
2253   local-scope
2254   assume-screen 10/width, 10/height
2255   # first line starts out wrapping
2256   s:text <- new [123456
2257 789]
2258   e:&:editor <- new-editor s, 0/left, 5/right
2259   editor-render screen, e
2260   screen-should-contain [
2261     .          .
2262     .1234↩     .
2263     .56        .
2264     .789       .
2265     .╌╌╌╌╌     .
2266     .          .
2267   ]
2268   $clear-trace
2269   # ctrl-u enough of the first line that it's no longer wrapping
2270   assume-console [
2271     left-click 1, 3
2272     press ctrl-u
2273   ]
2274   run [
2275     editor-event-loop screen, console, e
2276   ]
2277   # entire screen needs to be refreshed
2278   screen-should-contain [
2279     .          .
2280     .456       .
2281     .789       .
2282     .╌╌╌╌╌     .
2283     .          .
2284   ]
2285   check-trace-count-for-label 45, [print-character]
2286 ]
2287 
2288 # sometimes hitting ctrl-u needs to adjust the cursor row
2289 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-2 [
2290   local-scope
2291   assume-screen 10/width, 10/height
2292   # third line starts out wrapping
2293   s:text <- new [1
2294 2
2295 345678
2296 9]
2297   e:&:editor <- new-editor s, 0/left, 5/right
2298   editor-render screen, e
2299   screen-should-contain [
2300     .          .
2301     .1         .
2302     .2         .
2303     .3456↩     .
2304     .78        .
2305     .9         .
2306     .╌╌╌╌╌     .
2307     .          .
2308   ]
2309   # position cursor on screen line after the wrap and hit ctrl-u
2310   assume-console [
2311     left-click 4, 1  # on '8'
2312     press ctrl-u
2313   ]
2314   run [
2315     editor-event-loop screen, console, e
2316     10:num/raw <- get *e, cursor-row:offset
2317     11:num/raw <- get *e, cursor-column:offset
2318   ]
2319   screen-should-contain [
2320     .          .
2321     .1         .
2322     .2         .
2323     .8         .
2324     .9         .
2325     .╌╌╌╌╌     .
2326     .          .
2327   ]
2328   # cursor moves up one screen line
2329   memory-should-contain [
2330     10 <- 3  # cursor-row
2331     11 <- 0  # cursor-column
2332   ]
2333 ]
2334 
2335 # line wrapping twice (taking up 3 screen lines)
2336 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-3 [
2337   local-scope
2338   assume-screen 10/width, 10/height
2339   # third line starts out wrapping
2340   s:text <- new [1
2341 2
2342 3456789abcd
2343 e]
2344   e:&:editor <- new-editor s, 0/left, 5/right
2345   editor-render screen, e
2346   assume-console [
2347     left-click 4, 1  # on '8'
2348   ]
2349   editor-event-loop screen, console, e
2350   screen-should-contain [
2351     .          .
2352     .1         .
2353     .2         .
2354     .3456↩     .
2355     .789a↩     .
2356     .bcd       .
2357     .e         .
2358     .╌╌╌╌╌     .
2359     .          .
2360   ]
2361   assume-console [
2362     left-click 5, 1
2363     press ctrl-u
2364   ]
2365   run [
2366     editor-event-loop screen, console, e
2367     10:num/raw <- get *e, cursor-row:offset
2368     11:num/raw <- get *e, cursor-column:offset
2369   ]
2370   screen-should-contain [
2371     .          .
2372     .1         .
2373     .2         .
2374     .cd        .
2375     .e         .
2376     .╌╌╌╌╌     .
2377     .          .
2378   ]
2379   # make sure we adjusted cursor-row
2380   memory-should-contain [
2381     10 <- 3  # cursor-row
2382     11 <- 0  # cursor-column
2383   ]
2384 ]
2385 
2386 # adjusting cursor row at the top of the screen
2387 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-4 [
2388   local-scope
2389   assume-screen 10/width, 10/height
2390   # first line starts out wrapping
2391   s:text <- new [1234567
2392 89]
2393   e:&:editor <- new-editor s, 0/left, 5/right
2394   editor-render screen, e
2395   screen-should-contain [
2396     .          .
2397     .1234↩     .
2398     .567       .
2399     .89        .
2400     .╌╌╌╌╌     .
2401     .          .
2402   ]
2403   # position cursor on second screen line (after the wrap) and hit ctrl-u
2404   assume-console [
2405     left-click 2, 1
2406     press ctrl-u
2407   ]
2408   run [
2409     editor-event-loop screen, console, e
2410     10:num/raw <- get *e, cursor-row:offset
2411     11:num/raw <- get *e, cursor-column:offset
2412   ]
2413   screen-should-contain [
2414     .          .
2415     .67        .
2416     .89        .
2417     .╌╌╌╌╌     .
2418     .          .
2419   ]
2420   # cursor moves up to screen line 1
2421   memory-should-contain [
2422     10 <- 1  # cursor-row
2423     11 <- 0  # cursor-column
2424   ]
2425 ]
2426 
2427 # screen begins part-way through a wrapping line
2428 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-5 [
2429   local-scope
2430   assume-screen 10/width, 10/height
2431   # third line starts out wrapping
2432   s:text <- new [1
2433 2
2434 345678
2435 9]
2436   e:&:editor <- new-editor s, 0/left, 5/right
2437   editor-render screen, e
2438   # position the '78' line at the top of the screen
2439   assume-console [
2440     left-click 4, 1  # on '8'
2441     press ctrl-t
2442   ]
2443   editor-event-loop screen, console, e
2444   screen-should-contain [
2445     .          .
2446     .78        .
2447     .9         .
2448     .╌╌╌╌╌     .
2449     .          .
2450   ]
2451   assume-console [
2452     left-click 1, 1
2453     press ctrl-u
2454   ]
2455   run [
2456     editor-event-loop screen, console, e
2457     10:num/raw <- get *e, cursor-row:offset
2458     11:num/raw <- get *e, cursor-column:offset
2459   ]
2460   # make sure we updated top-of-screen correctly
2461   screen-should-contain [
2462     .          .
2463     .8         .
2464     .9         .
2465     .╌╌╌╌╌     .
2466     .          .
2467   ]
2468   memory-should-contain [
2469     10 <- 1  # cursor-row
2470     11 <- 0  # cursor-column
2471   ]
2472   # the entire line is deleted, even the part not shown on screen
2473   assume-console [
2474     press up-arrow
2475   ]
2476   run [
2477     editor-event-loop screen, console, e
2478   ]
2479   screen-should-contain [
2480     .          .
2481     .2         .
2482     .8         .
2483     .9         .
2484     .╌╌╌╌╌     .
2485     .          .
2486   ]
2487 ]
2488 
2489 # screen begins part-way through a line wrapping twice (taking up 3 screen lines)
2490 scenario editor-deletes-to-start-of-wrapped-line-with-ctrl-u-6 [
2491   local-scope
2492   assume-screen 10/width, 10/height
2493   # third line starts out wrapping
2494   s:text <- new [1
2495 2
2496 3456789abcd
2497 e]
2498   e:&:editor <- new-editor s, 0/left, 5/right
2499   editor-render screen, e
2500   # position the 'bcd' line at the top of the screen
2501   assume-console [
2502     left-click 4, 1  # on '8'
2503     press ctrl-t
2504     press ctrl-s  # now on 'c'
2505   ]
2506   editor-event-loop screen, console, e
2507   screen-should-contain [
2508     .          .
2509     .bcd       .
2510     .e         .
2511     .╌╌╌╌╌     .
2512     .          .
2513   ]
2514   assume-console [
2515     left-click 1, 1
2516     press ctrl-u
2517   ]
2518   run [
2519     editor-event-loop screen, console, e
2520     10:num/raw <- get *e, cursor-row:offset
2521     11:num/raw <- get *e, cursor-column:offset
2522   ]
2523   # make sure we updated top-of-screen correctly
2524   screen-should-contain [
2525     .          .
2526     .cd        .
2527     .e         .
2528     .╌╌╌╌╌     .
2529     .          .
2530   ]
2531   memory-should-contain [
2532     10 <- 1  # cursor-row
2533     11 <- 0  # cursor-column
2534   ]
2535   # the entire line is deleted, even the part not shown on screen
2536   assume-console [
2537     press up-arrow
2538   ]
2539   run [
2540     editor-event-loop screen, console, e
2541   ]
2542   screen-should-contain [
2543     .          .
2544     .2         .
2545     .cd        .
2546     .e         .
2547     .╌╌╌╌╌     .
2548     .          .
2549   ]
2550 ]
2551 
2552 # ctrl-k - delete text from cursor to end of line (but not the newline)
2553 
2554 scenario editor-deletes-to-end-of-line-with-ctrl-k [
2555   local-scope
2556   assume-screen 10/width, 5/height
2557   s:text <- new [123
2558 456]
2559   e:&:editor <- new-editor s, 0/left, 10/right
2560   editor-render screen, e
2561   $clear-trace
2562   # start on first line, press ctrl-k
2563   assume-console [
2564     left-click 1, 1
2565     press ctrl-k
2566   ]
2567   run [
2568     editor-event-loop screen, console, e
2569   ]
2570   # cursor deletes to end of line
2571   screen-should-contain [
2572     .          .
2573     .1         .
2574     .456       .
2575     .╌╌╌╌╌╌╌╌╌╌.
2576     .          .
2577   ]
2578   check-trace-count-for-label 9, [print-character]
2579 ]
2580 
2581 after <handle-special-character> [
2582   {
2583     delete-to-end-of-line?:bool <- equal c, 11/ctrl-k
2584     break-unless delete-to-end-of-line?
2585     <begin-delete-to-end-of-line>
2586     deleted-cells:&:duplex-list:char <- delete-to-end-of-line editor
2587     <end-delete-to-end-of-line>
2588     # checks if we can do a minimal render and if we can it will do a minimal render
2589     go-render?:bool <- minimal-render-for-ctrl-k screen, editor, deleted-cells
2590     return
2591   }
2592 ]
2593 
2594 def minimal-render-for-ctrl-k screen:&:screen, editor:&:editor, deleted-cells:&:duplex-list:char -> go-render?:bool, screen:&:screen [
2595   local-scope
2596   load-inputs
2597   # if we deleted nothing, there's nothing to render
2598   return-unless deleted-cells, false/dont-render
2599   # if the line used to wrap before, give up and render the whole screen
2600   curr-column:num <- get *editor, cursor-column:offset
2601   num-deleted-cells:num <- length deleted-cells
2602   old-row-len:num <- add curr-column, num-deleted-cells
2603   left:num <- get *editor, left:offset
2604   right:num <- get *editor, right:offset
2605   end:num <- subtract right, left
2606   wrap?:bool <- greater-or-equal old-row-len, end
2607   return-if wrap?, true/go-render
2608   clear-line-until screen, right
2609   return false/dont-render
2610 ]
2611 
2612 def delete-to-end-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
2613   local-scope
2614   load-inputs
2615   # compute range to delete
2616   start:&:duplex-list:char <- get *editor, before-cursor:offset
2617   end:&:duplex-list:char <- next start
2618   {
2619     at-end-of-text?:bool <- equal end, null
2620     break-if at-end-of-text?
2621     curr:char <- get *end, value:offset
2622     at-end-of-line?:bool <- equal curr, 10/newline
2623     break-if at-end-of-line?
2624     end <- next end
2625     loop
2626   }
2627   # snip it out
2628   result <- next start
2629   remove-between start, end
2630 ]
2631 
2632 scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [
2633   local-scope
2634   assume-screen 10/width, 5/height
2635   s:text <- new [123
2636 456]
2637   e:&:editor <- new-editor s, 0/left, 10/right
2638   editor-render screen, e
2639   $clear-trace
2640   # start on second line (no newline after), press ctrl-k
2641   assume-console [
2642     left-click 2, 1
2643     press ctrl-k
2644   ]
2645   run [
2646     editor-event-loop screen, console, e
2647   ]
2648   # cursor deletes to end of line
2649   screen-should-contain [
2650     .          .
2651     .123       .
2652     .4         .
2653     .╌╌╌╌╌╌╌╌╌╌.
2654     .          .
2655   ]
2656   check-trace-count-for-label 9, [print-character]
2657 ]
2658 
2659 scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [
2660   local-scope
2661   assume-screen 10/width, 5/height
2662   s:text <- new [123
2663 456]
2664   e:&:editor <- new-editor s, 0/left, 10/right
2665   editor-render screen, e
2666   $clear-trace
2667   # start at end of line
2668   assume-console [
2669     left-click 1, 2
2670     press ctrl-k
2671   ]
2672   run [
2673     editor-event-loop screen, console, e
2674   ]
2675   # cursor deletes just last character
2676   screen-should-contain [
2677     .          .
2678     .12        .
2679     .456       .
2680     .╌╌╌╌╌╌╌╌╌╌.
2681     .          .
2682   ]
2683   check-trace-count-for-label 8, [print-character]
2684 ]
2685 
2686 scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [
2687   local-scope
2688   assume-screen 10/width, 5/height
2689   s:text <- new [123
2690 456]
2691   e:&:editor <- new-editor s, 0/left, 10/right
2692   editor-render screen, e
2693   $clear-trace
2694   # start past end of line
2695   assume-console [
2696     left-click 1, 3
2697     press ctrl-k
2698   ]
2699   run [
2700     editor-event-loop screen, console, e
2701   ]
2702   # cursor deletes nothing
2703   screen-should-contain [
2704     .          .
2705     .123       .
2706     .456       .
2707     .╌╌╌╌╌╌╌╌╌╌.
2708     .          .
2709   ]
2710   check-trace-count-for-label 7, [print-character]
2711 ]
2712 
2713 scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [
2714   local-scope
2715   assume-screen 10/width, 5/height
2716   s:text <- new [123
2717 456]
2718   e:&:editor <- new-editor s, 0/left, 10/right
2719   editor-render screen, e
2720   $clear-trace
2721   # start at end of text
2722   assume-console [
2723     left-click 2, 2
2724     press ctrl-k
2725   ]
2726   run [
2727     editor-event-loop screen, console, e
2728   ]
2729   # cursor deletes just the final character
2730   screen-should-contain [
2731     .          .
2732     .123       .
2733     .45        .
2734     .╌╌╌╌╌╌╌╌╌╌.
2735     .          .
2736   ]
2737   check-trace-count-for-label 8, [print-character]
2738 ]
2739 
2740 scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [
2741   local-scope
2742   assume-screen 10/width, 5/height
2743   s:text <- new [123
2744 456]
2745   e:&:editor <- new-editor s, 0/left, 10/right
2746   editor-render screen, e
2747   $clear-trace
2748   # start past end of text
2749   assume-console [
2750     left-click 2, 3
2751     press ctrl-k
2752   ]
2753   run [
2754     editor-event-loop screen, console, e
2755   ]
2756   # cursor deletes nothing
2757   screen-should-contain [
2758     .          .
2759     .123       .
2760     .456       .
2761     .╌╌╌╌╌╌╌╌╌╌.
2762     .          .
2763   ]
2764   # no prints necessary
2765   check-trace-count-for-label 0, [print-character]
2766 ]
2767 
2768 scenario editor-deletes-to-end-of-wrapped-line-with-ctrl-k [
2769   local-scope
2770   assume-screen 10/width, 5/height
2771   # create an editor with the first line wrapping to a second screen row
2772   s:text <- new [1234
2773 567]
2774   e:&:editor <- new-editor s, 0/left, 4/right
2775   editor-render screen, e
2776   $clear-trace
2777   # delete all of the first wrapped line
2778   assume-console [
2779     press ctrl-k
2780   ]
2781   run [
2782     editor-event-loop screen, console, e
2783   ]
2784   # screen shows an empty unwrapped first line
2785   screen-should-contain [
2786     .          .
2787     .          .
2788     .567       .
2789     .╌╌╌╌      .
2790     .          .
2791   ]
2792   # entire screen is refreshed
2793   check-trace-count-for-label 16, [print-character]
2794 ]
2795 
2796 # scroll down if necessary
2797 
2798 scenario editor-can-scroll-down-using-arrow-keys [
2799   local-scope
2800   # screen has 1 line for menu + 3 lines
2801   assume-screen 10/width, 4/height
2802   # initialize editor with >3 lines
2803   s:text <- new [a
2804 b
2805 c
2806 d]
2807   e:&:editor <- new-editor s, 0/left, 10/right
2808   editor-render screen, e
2809   screen-should-contain [
2810     .          .
2811     .a         .
2812     .b         .
2813     .c         .
2814   ]
2815   # position cursor at last line, then try to move further down
2816   assume-console [
2817     left-click 3, 0
2818     press down-arrow
2819   ]
2820   run [
2821     editor-event-loop screen, console, e
2822   ]
2823   # screen slides by one line
2824   screen-should-contain [
2825     .          .
2826     .b         .
2827     .c         .
2828     .d         .
2829   ]
2830 ]
2831 
2832 after <scroll-down> [
2833   trace 10, [app], [scroll down]
2834   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2835   left:num <- get *editor, left:offset
2836   right:num <- get *editor, right:offset
2837   max:num <- subtract right, left
2838   old-top:&:duplex-list:char <- copy top-of-screen
2839   top-of-screen <- before-start-of-next-line top-of-screen, max
2840   *editor <- put *editor, top-of-screen:offset, top-of-screen
2841   no-movement?:bool <- equal old-top, top-of-screen
2842   return-if no-movement?, false/don't-render
2843 ]
2844 
2845 after <scroll-down2> [
2846   trace 10, [app], [scroll down]
2847   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2848   left:num <- get *editor, left:offset
2849   right:num <- get *editor, right:offset
2850   max:num <- subtract right, left
2851   old-top:&:duplex-list:char <- copy top-of-screen
2852   top-of-screen <- before-start-of-next-line top-of-screen, max
2853   *editor <- put *editor, top-of-screen:offset, top-of-screen
2854   no-movement?:bool <- equal old-top, top-of-screen
2855   return-if no-movement?
2856 ]
2857 
2858 # Takes a pointer into the doubly-linked list, scans ahead at most 'max'
2859 # positions until the next newline.
2860 # Returns original if no next newline.
2861 # Beware: never return null pointer.
2862 def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [
2863   local-scope
2864   load-inputs
2865   count:num <- copy 0
2866   curr:&:duplex-list:char <- copy original
2867   # skip the initial newline if it exists
2868   {
2869     c:char <- get *curr, value:offset
2870     at-newline?:bool <- equal c, 10/newline
2871     break-unless at-newline?
2872     curr <- next curr
2873     count <- add count, 1
2874   }
2875   {
2876     return-unless curr, original
2877     done?:bool <- greater-or-equal count, max
2878     break-if done?
2879     c:char <- get *curr, value:offset
2880     at-newline?:bool <- equal c, 10/newline
2881     break-if at-newline?
2882     curr <- next curr
2883     count <- add count, 1
2884     loop
2885   }
2886   return-unless curr, original
2887   return curr
2888 ]
2889 
2890 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys [
2891   local-scope
2892   # screen has 1 line for menu + 3 lines
2893   assume-screen 10/width, 4/height
2894   # initialize editor with a long, wrapped line and more than a screen of
2895   # other lines
2896   s:text <- new [abcdef
2897 g
2898 h
2899 i]
2900   e:&:editor <- new-editor s, 0/left, 5/right
2901   editor-render screen, e
2902   screen-should-contain [
2903     .          .
2904     .abcd↩     .
2905     .ef        .
2906     .g         .
2907   ]
2908   # position cursor at last line, then try to move further down
2909   assume-console [
2910     left-click 3, 0
2911     press down-arrow
2912   ]
2913   run [
2914     editor-event-loop screen, console, e
2915   ]
2916   # screen shows partial wrapped line
2917   screen-should-contain [
2918     .          .
2919     .ef        .
2920     .g         .
2921     .h         .
2922   ]
2923 ]
2924 
2925 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys-2 [
2926   local-scope
2927   # screen has 1 line for menu + 3 lines
2928   assume-screen 10/width, 4/height
2929   # editor starts with a long line wrapping twice
2930   s:text <- new [abcdefghij
2931 k
2932 l
2933 m]
2934   e:&:editor <- new-editor s, 0/left, 5/right
2935   # position cursor at last line, then try to move further down
2936   assume-console [
2937     left-click 3, 0
2938     press down-arrow
2939   ]
2940   run [
2941     editor-event-loop screen, console, e
2942   ]
2943   # screen shows partial wrapped line containing a wrap icon
2944   screen-should-contain [
2945     .          .
2946     .efgh↩     .
2947     .ij        .
2948     .k         .
2949   ]
2950   # scroll down again
2951   assume-console [
2952     press down-arrow
2953   ]
2954   run [
2955     editor-event-loop screen, console, e
2956   ]
2957   # screen shows partial wrapped line
2958   screen-should-contain [
2959     .          .
2960     .ij        .
2961     .k         .
2962     .l         .
2963   ]
2964 ]
2965 
2966 scenario editor-scrolls-down-when-line-wraps [
2967   local-scope
2968   # screen has 1 line for menu + 3 lines
2969   assume-screen 5/width, 4/height
2970   # editor contains a long line in the third line
2971   s:text <- new [a
2972 b
2973 cdef]
2974   e:&:editor <- new-editor s, 0/left, 5/right
2975   # position cursor at end, type a character
2976   assume-console [
2977     left-click 3, 4
2978     type [g]
2979   ]
2980   run [
2981     editor-event-loop screen, console, e
2982     3:num/raw <- get *e, cursor-row:offset
2983     4:num/raw <- get *e, cursor-column:offset
2984   ]
2985   # screen scrolls
2986   screen-should-contain [
2987     .     .
2988     .b    .
2989     .cdef↩.
2990     .g    .
2991   ]
2992   memory-should-contain [
2993     3 <- 3
2994     4 <- 1
2995   ]
2996 ]
2997 
2998 scenario editor-stops-scrolling-once-bottom-is-visible [
2999   local-scope
3000   # screen has 1 line for menu + 3 lines
3001   assume-screen 10/width, 4/height
3002   # initialize editor with 2 lines
3003   s:text <- new [a
3004 b]
3005   e:&:editor <- new-editor s, 0/left, 10/right
3006   editor-render screen, e
3007   screen-should-contain [
3008     .          .
3009     .a         .
3010     .b         .
3011     .╌╌╌╌╌╌╌╌╌╌.
3012   ]
3013   # position cursor at last line, then try to move further down
3014   assume-console [
3015     left-click 3, 0
3016     press down-arrow
3017   ]
3018   run [
3019     editor-event-loop screen, console, e
3020   ]
3021   # no change since the bottom border was already visible
3022   screen-should-contain [
3023     .          .
3024     .a         .
3025     .b         .
3026     .╌╌╌╌╌╌╌╌╌╌.
3027   ]
3028 ]
3029 
3030 scenario editor-scrolls-down-on-newline [
3031   local-scope
3032   assume-screen 5/width, 4/height
3033   # position cursor after last line and type newline
3034   s:text <- new [a
3035 b
3036 c]
3037   e:&:editor <- new-editor s, 0/left, 5/right
3038   assume-console [
3039     left-click 3, 4
3040     type [
3041 ]
3042   ]
3043   run [
3044     editor-event-loop screen, console, e
3045     3:num/raw <- get *e, cursor-row:offset
3046     4:num/raw <- get *e, cursor-column:offset
3047   ]
3048   # screen scrolls
3049   screen-should-contain [
3050     .     .
3051     .b    .
3052     .c    .
3053     .     .
3054   ]
3055   memory-should-contain [
3056     3 <- 3
3057     4 <- 0
3058   ]
3059 ]
3060 
3061 scenario editor-scrolls-down-on-right-arrow [
3062   local-scope
3063   # screen has 1 line for menu + 3 lines
3064   assume-screen 5/width, 4/height
3065   # editor contains a wrapped line
3066   s:text <- new [a
3067 b
3068 cdefgh]
3069   e:&:editor <- new-editor s, 0/left, 5/right
3070   # position cursor at end of screen and try to move right
3071   assume-console [
3072     left-click 3, 3
3073     press right-arrow
3074   ]
3075   run [
3076     editor-event-loop screen, console, e
3077     3:num/raw <- get *e, cursor-row:offset
3078     4:num/raw <- get *e, cursor-column:offset
3079   ]
3080   # screen scrolls
3081   screen-should-contain [
3082     .     .
3083     .b    .
3084     .cdef↩.
3085     .gh   .
3086   ]
3087   memory-should-contain [
3088     3 <- 3
3089     4 <- 0
3090   ]
3091 ]
3092 
3093 scenario editor-scrolls-down-on-right-arrow-2 [
3094   local-scope
3095   # screen has 1 line for menu + 3 lines
3096   assume-screen 5/width, 4/height
3097   # editor contains more lines than can fit on screen
3098   s:text <- new [a
3099 b
3100 c
3101 d]
3102   e:&:editor <- new-editor s, 0/left, 5/right
3103   # position cursor at end of screen and try to move right
3104   assume-console [
3105     left-click 3, 3
3106     press right-arrow
3107   ]
3108   run [
3109     editor-event-loop screen, console, e
3110     3:num/raw <- get *e, cursor-row:offset
3111     4:num/raw <- get *e, cursor-column:offset
3112   ]
3113   # screen scrolls
3114   screen-should-contain [
3115     .     .
3116     .b    .
3117     .c    .
3118     .d    .
3119   ]
3120   memory-should-contain [
3121     3 <- 3
3122     4 <- 0
3123   ]
3124 ]
3125 
3126 scenario editor-scrolls-at-end-on-down-arrow [
3127   local-scope
3128   assume-screen 10/width, 5/height
3129   s:text <- new [abc
3130 de]
3131   e:&:editor <- new-editor s, 0/left, 10/right
3132   editor-render screen, e
3133   $clear-trace
3134   # try to move down past end of text
3135   assume-console [
3136     left-click 2, 0
3137     press down-arrow
3138   ]
3139   run [
3140     editor-event-loop screen, console, e
3141     3:num/raw <- get *e, cursor-row:offset
3142     4:num/raw <- get *e, cursor-column:offset
3143   ]
3144   # no change
3145   memory-should-contain [
3146     3 <- 2
3147     4 <- 0
3148   ]
3149 ]
3150 
3151 scenario editor-combines-page-and-line-scroll [
3152   local-scope
3153   # screen has 1 line for menu + 3 lines
3154   assume-screen 10/width, 4/height
3155   # initialize editor with a few pages of lines
3156   s:text <- new [a
3157 b
3158 c
3159 d
3160 e
3161 f
3162 g]
3163   e:&:editor <- new-editor s, 0/left, 5/right
3164   editor-render screen, e
3165   # scroll down one page and one line
3166   assume-console [
3167     press page-down
3168     left-click 3, 0
3169     press down-arrow
3170   ]
3171   run [
3172     editor-event-loop screen, console, e
3173   ]
3174   # screen scrolls down 3 lines
3175   screen-should-contain [
3176     .          .
3177     .d         .
3178     .e         .
3179     .f         .
3180   ]
3181 ]
3182 
3183 # scroll up if necessary
3184 
3185 scenario editor-can-scroll-up-using-arrow-keys [
3186   local-scope
3187   # screen has 1 line for menu + 3 lines
3188   assume-screen 10/width, 4/height
3189   # initialize editor with >3 lines
3190   s:text <- new [a
3191 b
3192 c
3193 d]
3194   e:&:editor <- new-editor s, 0/left, 10/right
3195   editor-render screen, e
3196   screen-should-contain [
3197     .          .
3198     .a         .
3199     .b         .
3200     .c         .
3201   ]
3202   # position cursor at top of second page, then try to move up
3203   assume-console [
3204     press page-down
3205     press up-arrow
3206   ]
3207   run [
3208     editor-event-loop screen, console, e
3209   ]
3210   # screen slides by one line
3211   screen-should-contain [
3212     .          .
3213     .b         .
3214     .c         .
3215     .d         .
3216   ]
3217 ]
3218 
3219 after <scroll-up> [
3220   trace 10, [app], [scroll up]
3221   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3222   old-top:&:duplex-list:char <- copy top-of-screen
3223   top-of-screen <- before-previous-screen-line top-of-screen, editor
3224   *editor <- put *editor, top-of-screen:offset, top-of-screen
3225   no-movement?:bool <- equal old-top, top-of-screen
3226   return-if no-movement?, false/don't-render
3227 ]
3228 
3229 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys [
3230   local-scope
3231   # screen has 1 line for menu + 3 lines
3232   assume-screen 10/width, 4/height
3233   # initialize editor with a long, wrapped line and more than a screen of
3234   # other lines
3235   s:text <- new [abcdef
3236 g
3237 h
3238 i]
3239   e:&:editor <- new-editor s, 0/left, 5/right
3240   editor-render screen, e
3241   screen-should-contain [
3242     .          .
3243     .abcd↩     .
3244     .ef        .
3245     .g         .
3246   ]
3247   # position cursor at top of second page, just below wrapped line
3248   assume-console [
3249     press page-down
3250   ]
3251   run [
3252     editor-event-loop screen, console, e
3253   ]
3254   screen-should-contain [
3255     .          .
3256     .g         .
3257     .h         .
3258     .i         .
3259   ]
3260   # now move up one line
3261   assume-console [
3262     press up-arrow
3263   ]
3264   run [
3265     editor-event-loop screen, console, e
3266   ]
3267   # screen shows partial wrapped line
3268   screen-should-contain [
3269     .          .
3270     .ef        .
3271     .g         .
3272     .h         .
3273   ]
3274 ]
3275 
3276 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-2 [
3277   local-scope
3278   # screen has 1 line for menu + 4 lines
3279   assume-screen 10/width, 5/height
3280   # editor starts with a long line wrapping twice, occupying 3 of the 4 lines
3281   s:text <- new [abcdefghij
3282 k
3283 l
3284 m]
3285   e:&:editor <- new-editor s, 0/left, 5/right
3286   editor-render screen, e
3287   # position cursor at top of second page
3288   assume-console [
3289     press page-down
3290   ]
3291   run [
3292     editor-event-loop screen, console, e
3293   ]
3294   screen-should-contain [
3295     .          .
3296     .k         .
3297     .l         .
3298     .m         .
3299     .╌╌╌╌╌     .
3300   ]
3301   # move up one line
3302   assume-console [
3303     press up-arrow
3304   ]
3305   run [
3306     editor-event-loop screen, console, e
3307   ]
3308   # screen shows partial wrapped line
3309   screen-should-contain [
3310     .          .
3311     .ij        .
3312     .k         .
3313     .l         .
3314     .m         .
3315   ]
3316   # move up a second line
3317   assume-console [
3318     press up-arrow
3319   ]
3320   run [
3321     editor-event-loop screen, console, e
3322   ]
3323   # screen shows partial wrapped line
3324   screen-should-contain [
3325     .          .
3326     .efgh↩     .
3327     .ij        .
3328     .k         .
3329     .l         .
3330   ]
3331   # move up a third line
3332   assume-console [
3333     press up-arrow
3334   ]
3335   run [
3336     editor-event-loop screen, console, e
3337   ]
3338   # screen shows partial wrapped line
3339   screen-should-contain [
3340     .          .
3341     .abcd↩     .
3342     .efgh↩     .
3343     .ij        .
3344     .k         .
3345   ]
3346 ]
3347 
3348 # same as editor-scrolls-up-past-wrapped-line-using-arrow-keys but length
3349 # slightly off, just to prevent over-training
3350 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-3 [
3351   local-scope
3352   # screen has 1 line for menu + 3 lines
3353   assume-screen 10/width, 4/height
3354   # initialize editor with a long, wrapped line and more than a screen of
3355   # other lines
3356   s:text <- new [abcdef
3357 g
3358 h
3359 i]
3360   e:&:editor <- new-editor s, 0/left, 6/right
3361   editor-render screen, e
3362   screen-should-contain [
3363     .          .
3364     .abcde↩    .
3365     .f         .
3366     .g         .
3367   ]
3368   # position cursor at top of second page, just below wrapped line
3369   assume-console [
3370     press page-down
3371   ]
3372   run [
3373     editor-event-loop screen, console, e
3374   ]
3375   screen-should-contain [
3376     .          .
3377     .g         .
3378     .h         .
3379     .i         .
3380   ]
3381   # now move up one line
3382   assume-console [
3383     press up-arrow
3384   ]
3385   run [
3386     editor-event-loop screen, console, e
3387   ]
3388   # screen shows partial wrapped line
3389   screen-should-contain [
3390     .          .
3391     .f         .
3392     .g         .
3393     .h         .
3394   ]
3395 ]
3396 
3397 # check empty lines
3398 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-4 [
3399   local-scope
3400   assume-screen 10/width, 4/height
3401   # initialize editor with some lines around an empty line
3402   s:text <- new [a
3403 b
3404 
3405 c
3406 d
3407 e]
3408   e:&:editor <- new-editor s, 0/left, 6/right
3409   editor-render screen, e
3410   assume-console [
3411     press page-down
3412   ]
3413   run [
3414     editor-event-loop screen, console, e
3415   ]
3416   screen-should-contain [
3417     .          .
3418     .          .
3419     .c         .
3420     .d         .
3421   ]
3422   assume-console [
3423     press page-down
3424   ]
3425   run [
3426     editor-event-loop screen, console, e
3427   ]
3428   screen-should-contain [
3429     .          .
3430     .d         .
3431     .e         .
3432     .╌╌╌╌╌╌    .
3433   ]
3434   assume-console [
3435     press page-up
3436   ]
3437   run [
3438     editor-event-loop screen, console, e
3439   ]
3440   screen-should-contain [
3441     .          .
3442     .          .
3443     .c         .
3444     .d         .
3445   ]
3446 ]
3447 
3448 scenario editor-scrolls-up-on-left-arrow [
3449   local-scope
3450   # screen has 1 line for menu + 3 lines
3451   assume-screen 5/width, 4/height
3452   # editor contains >3 lines
3453   s:text <- new [a
3454 b
3455 c
3456 d
3457 e]
3458   e:&:editor <- new-editor s, 0/left, 5/right
3459   editor-render screen, e
3460   # position cursor at top of second page
3461   assume-console [
3462     press page-down
3463   ]
3464   run [
3465     editor-event-loop screen, console, e
3466   ]
3467   screen-should-contain [
3468     .     .
3469     .c    .
3470     .d    .
3471     .e    .
3472   ]
3473   # now try to move left
3474   assume-console [
3475     press left-arrow
3476   ]
3477   run [
3478     editor-event-loop screen, console, e
3479     3:num/raw <- get *e, cursor-row:offset
3480     4:num/raw <- get *e, cursor-column:offset
3481   ]
3482   # screen scrolls
3483   screen-should-contain [
3484     .     .
3485     .b    .
3486     .c    .
3487     .d    .
3488   ]
3489   memory-should-contain [
3490     3 <- 1
3491     4 <- 1
3492   ]
3493 ]
3494 
3495 scenario editor-can-scroll-up-to-start-of-file [
3496   local-scope
3497   # screen has 1 line for menu + 3 lines
3498   assume-screen 10/width, 4/height
3499   # initialize editor with >3 lines
3500   s:text <- new [a
3501 b
3502 c
3503 d]
3504   e:&:editor <- new-editor s, 0/left, 10/right
3505   editor-render screen, e
3506   screen-should-contain [
3507     .          .
3508     .a         .
3509     .b         .
3510     .c         .
3511   ]
3512   # position cursor at top of second page, then try to move up to start of
3513   # text
3514   assume-console [
3515     press page-down
3516     press up-arrow
3517     press up-arrow
3518   ]
3519   run [
3520     editor-event-loop screen, console, e
3521   ]
3522   # screen slides by one line
3523   screen-should-contain [
3524     .          .
3525     .a         .
3526     .b         .
3527     .c         .
3528   ]
3529   # try to move up again
3530   assume-console [
3531     press up-arrow
3532   ]
3533   run [
3534     editor-event-loop screen, console, e
3535   ]
3536   # screen remains unchanged
3537   screen-should-contain [
3538     .          .
3539     .a         .
3540     .b         .
3541     .c         .
3542   ]
3543 ]
3544 
3545 # ctrl-f/page-down - render next page if it exists
3546 
3547 scenario editor-can-scroll [
3548   local-scope
3549   assume-screen 10/width, 4/height
3550   s:text <- new [a
3551 b
3552 c
3553 d]
3554   e:&:editor <- new-editor s, 0/left, 10/right
3555   editor-render screen, e
3556   screen-should-contain [
3557     .          .
3558     .a         .
3559     .b         .
3560     .c         .
3561   ]
3562   # scroll down
3563   assume-console [
3564     press page-down
3565   ]
3566   run [
3567     editor-event-loop screen, console, e
3568   ]
3569   # screen shows next page
3570   screen-should-contain [
3571     .          .
3572     .c         .
3573     .d         .
3574     .╌╌╌╌╌╌╌╌╌╌.
3575   ]
3576 ]
3577 
3578 after <handle-special-character> [
3579   {
3580     page-down?:bool <- equal c, 6/ctrl-f
3581     break-unless page-down?
3582     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3583     <begin-move-cursor>
3584     page-down editor
3585     undo-coalesce-tag:num <- copy 0/never
3586     <end-move-cursor>
3587     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3588     movement?:bool <- not-equal top-of-screen, old-top
3589     return movement?/go-render
3590   }
3591 ]
3592 
3593 after <handle-special-key> [
3594   {
3595     page-down?:bool <- equal k, 65518/page-down
3596     break-unless page-down?
3597     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3598     <begin-move-cursor>
3599     page-down editor
3600     undo-coalesce-tag:num <- copy 0/never
3601     <end-move-cursor>
3602     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3603     movement?:bool <- not-equal top-of-screen, old-top
3604     return movement?/go-render
3605   }
3606 ]
3607 
3608 # page-down skips entire wrapped lines, so it can't scroll past lines
3609 # taking up the entire screen
3610 def page-down editor:&:editor -> editor:&:editor [
3611   local-scope
3612   load-inputs
3613   # if editor contents don't overflow screen, do nothing
3614   bottom-of-screen:&:duplex-list:char <- get *editor, bottom-of-screen:offset
3615   return-unless bottom-of-screen
3616   # if not, position cursor at final character
3617   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
3618   before-cursor:&:duplex-list:char <- prev bottom-of-screen
3619   *editor <- put *editor, before-cursor:offset, before-cursor
3620   # keep one line in common with previous page
3621   {
3622     last:char <- get *before-cursor, value:offset
3623     newline?:bool <- equal last, 10/newline
3624     break-unless newline?:bool
3625     before-cursor <- prev before-cursor
3626     *editor <- put *editor, before-cursor:offset, before-cursor
3627   }
3628   # move cursor and top-of-screen to start of that line
3629   move-to-start-of-line editor
3630   before-cursor <- get *editor, before-cursor:offset
3631   *editor <- put *editor, top-of-screen:offset, before-cursor
3632 ]
3633 
3634 # jump to previous newline
3635 def move-to-start-of-line editor:&:editor -> editor:&:editor [
3636   local-scope
3637   load-inputs
3638   # update cursor column
3639   left:num <- get *editor, left:offset
3640   cursor-column:num <- copy left
3641   *editor <- put *editor, cursor-column:offset, cursor-column
3642   # update before-cursor
3643   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
3644   init:&:duplex-list:char <- get *editor, data:offset
3645   # while not at start of line, move
3646   {
3647     at-start-of-text?:bool <- equal before-cursor, init
3648     break-if at-start-of-text?
3649     prev:char <- get *before-cursor, value:offset
3650     at-start-of-line?:bool <- equal prev, 10/newline
3651     break-if at-start-of-line?
3652     before-cursor <- prev before-cursor
3653     *editor <- put *editor, before-cursor:offset, before-cursor
3654     assert before-cursor, [move-to-start-of-line tried to move before start of text]
3655     loop
3656   }
3657 ]
3658 
3659 scenario editor-does-not-scroll-past-end [
3660   local-scope
3661   assume-screen 10/width, 4/height
3662   s:text <- new [a
3663 b]
3664   e:&:editor <- new-editor s, 0/left, 10/right
3665   editor-render screen, e
3666   screen-should-contain [
3667     .          .
3668     .a         .
3669     .b         .
3670     .╌╌╌╌╌╌╌╌╌╌.
3671   ]
3672   # scroll down
3673   assume-console [
3674     press page-down
3675   ]
3676   run [
3677     editor-event-loop screen, console, e
3678   ]
3679   # screen remains unmodified
3680   screen-should-contain [
3681     .          .
3682     .a         .
3683     .b         .
3684     .╌╌╌╌╌╌╌╌╌╌.
3685   ]
3686 ]
3687 
3688 scenario editor-starts-next-page-at-start-of-wrapped-line [
3689   local-scope
3690   # screen has 1 line for menu + 3 lines for text
3691   assume-screen 10/width, 4/height
3692   # editor contains a long last line
3693   s:text <- new [a
3694 b
3695 cdefgh]
3696   # editor screen triggers wrap of last line
3697   e:&:editor <- new-editor s, 0/left, 4/right
3698   editor-render screen, e
3699   # some part of last line is not displayed
3700   screen-should-contain [
3701     .          .
3702     .a         .
3703     .b         .
3704     .cde↩      .
3705   ]
3706   # scroll down
3707   assume-console [
3708     press page-down
3709   ]
3710   run [
3711     editor-event-loop screen, console, e
3712   ]
3713   # screen shows entire wrapped line
3714   screen-should-contain [
3715     .          .
3716     .cde↩      .
3717     .fgh       .
3718     .╌╌╌╌      .
3719   ]
3720 ]
3721 
3722 scenario editor-starts-next-page-at-start-of-wrapped-line-2 [
3723   local-scope
3724   # screen has 1 line for menu + 3 lines for text
3725   assume-screen 10/width, 4/height
3726   # editor contains a very long line that occupies last two lines of screen
3727   # and still has something left over
3728   s:text <- new [a
3729 bcdefgh]
3730   e:&:editor <- new-editor s, 0/left, 4/right
3731   editor-render screen, e
3732   # some part of last line is not displayed
3733   screen-should-contain [
3734     .          .
3735     .a         .
3736     .bcd↩      .
3737     .efg↩      .
3738   ]
3739   # scroll down
3740   assume-console [
3741     press page-down
3742   ]
3743   run [
3744     editor-event-loop screen, console, e
3745   ]
3746   # screen shows entire wrapped line
3747   screen-should-contain [
3748     .          .
3749     .bcd↩      .
3750     .efg↩      .
3751     .h         .
3752   ]
3753 ]
3754 
3755 # ctrl-b/page-up - render previous page if it exists
3756 
3757 scenario editor-can-scroll-up [
3758   local-scope
3759   assume-screen 10/width, 4/height
3760   s:text <- new [a
3761 b
3762 c
3763 d]
3764   e:&:editor <- new-editor s, 0/left, 10/right
3765   editor-render screen, e
3766   screen-should-contain [
3767     .          .
3768     .a         .
3769     .b         .
3770     .c         .
3771   ]
3772   # scroll down
3773   assume-console [
3774     press page-down
3775   ]
3776   run [
3777     editor-event-loop screen, console, e
3778   ]
3779   # screen shows next page
3780   screen-should-contain [
3781     .          .
3782     .c         .
3783     .d         .
3784     .╌╌╌╌╌╌╌╌╌╌.
3785   ]
3786   # scroll back up
3787   assume-console [
3788     press page-up
3789   ]
3790   run [
3791     editor-event-loop screen, console, e
3792   ]
3793   # screen shows original page again
3794   screen-should-contain [
3795     .          .
3796     .a         .
3797     .b         .
3798     .c         .
3799   ]
3800 ]
3801 
3802 after <handle-special-character> [
3803   {
3804     page-up?:bool <- equal c, 2/ctrl-b
3805     break-unless page-up?
3806     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3807     <begin-move-cursor>
3808     editor <- page-up editor, screen-height
3809     undo-coalesce-tag:num <- copy 0/never
3810     <end-move-cursor>
3811     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3812     movement?:bool <- not-equal top-of-screen, old-top
3813     return movement?/go-render
3814   }
3815 ]
3816 
3817 after <handle-special-key> [
3818   {
3819     page-up?:bool <- equal k, 65519/page-up
3820     break-unless page-up?
3821     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3822     <begin-move-cursor>
3823     editor <- page-up editor, screen-height
3824     undo-coalesce-tag:num <- copy 0/never
3825     <end-move-cursor>
3826     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3827     movement?:bool <- not-equal top-of-screen, old-top
3828     # don't bother re-rendering if nothing changed. todo: test this
3829     return movement?/go-render
3830   }
3831 ]
3832 
3833 def page-up editor:&:editor, screen-height:num -> editor:&:editor [
3834   local-scope
3835   load-inputs
3836   max:num <- subtract screen-height, 1/menu-bar, 1/overlapping-line
3837   count:num <- copy 0
3838   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3839   {
3840     done?:bool <- greater-or-equal count, max
3841     break-if done?
3842     prev:&:duplex-list:char <- before-previous-screen-line top-of-screen, editor
3843     break-unless prev
3844     top-of-screen <- copy prev
3845     *editor <- put *editor, top-of-screen:offset, top-of-screen
3846     count <- add count, 1
3847     loop
3848   }
3849 ]
3850 
3851 scenario editor-can-scroll-up-multiple-pages [
3852   local-scope
3853   # screen has 1 line for menu + 3 lines
3854   assume-screen 10/width, 4/height
3855   # initialize editor with 8 lines
3856   s:text <- new [a
3857 b
3858 c
3859 d
3860 e
3861 f
3862 g
3863 h]
3864   e:&:editor <- new-editor s, 0/left, 10/right
3865   editor-render screen, e
3866   screen-should-contain [
3867     .          .
3868     .a         .
3869     .b         .
3870     .c         .
3871   ]
3872   # scroll down two pages
3873   assume-console [
3874     press page-down
3875     press page-down
3876   ]
3877   run [
3878     editor-event-loop screen, console, e
3879   ]
3880   # screen shows third page
3881   screen-should-contain [
3882     .          .
3883     .e         .
3884     .f         .
3885     .g         .
3886   ]
3887   # scroll up
3888   assume-console [
3889     press page-up
3890   ]
3891   run [
3892     editor-event-loop screen, console, e
3893   ]
3894   # screen shows second page
3895   screen-should-contain [
3896     .          .
3897     .c         .
3898     .d         .
3899     .e         .
3900   ]
3901   # scroll up again
3902   assume-console [
3903     press page-up
3904   ]
3905   run [
3906     editor-event-loop screen, console, e
3907   ]
3908   # screen shows original page again
3909   screen-should-contain [
3910     .          .
3911     .a         .
3912     .b         .
3913     .c         .
3914   ]
3915 ]
3916 
3917 scenario editor-can-scroll-up-wrapped-lines [
3918   local-scope
3919   # screen has 1 line for menu + 5 lines for text
3920   assume-screen 10/width, 6/height
3921   # editor contains a long line in the first page
3922   s:text <- new [a
3923 b
3924 cdefgh
3925 i
3926 j
3927 k
3928 l
3929 m
3930 n
3931 o]
3932   # editor screen triggers wrap of last line
3933   e:&:editor <- new-editor s, 0/left, 4/right
3934   editor-render screen, e
3935   # some part of last line is not displayed
3936   screen-should-contain [
3937     .          .
3938     .a         .
3939     .b         .
3940     .cde↩      .
3941     .fgh       .
3942     .i         .
3943   ]
3944   # scroll down a page and a line
3945   assume-console [
3946     press page-down
3947     left-click 5, 0
3948     press down-arrow
3949   ]
3950   run [
3951     editor-event-loop screen, console, e
3952   ]
3953   # screen shows entire wrapped line
3954   screen-should-contain [
3955     .          .
3956     .j         .
3957     .k         .
3958     .l         .
3959     .m         .
3960     .n         .
3961   ]
3962   # now scroll up one page
3963   assume-console [
3964     press page-up
3965   ]
3966   run [
3967     editor-event-loop screen, console, e
3968   ]
3969   # screen resets
3970   screen-should-contain [
3971     .          .
3972     .b         .
3973     .cde↩      .
3974     .fgh       .
3975     .i         .
3976     .j         .
3977   ]
3978 ]
3979 
3980 scenario editor-can-scroll-up-wrapped-lines-2 [
3981   local-scope
3982   # screen has 1 line for menu + 3 lines for text
3983   assume-screen 10/width, 4/height
3984   # editor contains a very long line that occupies last two lines of screen
3985   # and still has something left over
3986   s:text <- new [a
3987 bcdefgh]
3988   e:&:editor <- new-editor s, 0/left, 4/right
3989   editor-render screen, e
3990   # some part of last line is not displayed
3991   screen-should-contain [
3992     .          .
3993     .a         .
3994     .bcd↩      .
3995     .efg↩      .
3996   ]
3997   # scroll down
3998   assume-console [
3999     press page-down
4000   ]
4001   run [
4002     editor-event-loop screen, console, e
4003   ]
4004   # screen shows entire wrapped line
4005   screen-should-contain [
4006     .          .
4007     .bcd↩      .
4008     .efg↩      .
4009     .h         .
4010   ]
4011   # scroll back up
4012   assume-console [
4013     press page-up
4014   ]
4015   run [
4016     editor-event-loop screen, console, e
4017   ]
4018   # screen resets
4019   screen-should-contain [
4020     .          .
4021     .a         .
4022     .bcd↩      .
4023     .efg↩      .
4024   ]
4025 ]
4026 
4027 scenario editor-can-scroll-up-past-nonempty-lines [
4028   local-scope
4029   assume-screen 10/width, 4/height
4030   # text with empty line in second screen
4031   s:text <- new [axx
4032 bxx
4033 cxx
4034 dxx
4035 exx
4036 fxx
4037 gxx
4038 hxx
4039 ]
4040   e:&:editor <- new-editor s, 0/left, 4/right
4041   editor-render screen, e
4042   screen-should-contain [
4043     .          .
4044     .axx       .
4045     .bxx       .
4046     .cxx       .
4047   ]
4048   assume-console [
4049     press page-down
4050   ]
4051   run [
4052     editor-event-loop screen, console, e
4053   ]
4054   screen-should-contain [
4055     .          .
4056     .cxx       .
4057     .dxx       .
4058     .exx       .
4059   ]
4060   assume-console [
4061     press page-down
4062   ]
4063   run [
4064     editor-event-loop screen, console, e
4065   ]
4066   screen-should-contain [
4067     .          .
4068     .exx       .
4069     .fxx       .
4070     .gxx       .
4071   ]
4072   # scroll back up past empty line
4073   assume-console [
4074     press page-up
4075   ]
4076   run [
4077     editor-event-loop screen, console, e
4078   ]
4079   screen-should-contain [
4080     .          .
4081     .cxx       .
4082     .dxx       .
4083     .exx       .
4084   ]
4085 ]
4086 
4087 scenario editor-can-scroll-up-past-empty-lines [
4088   local-scope
4089   assume-screen 10/width, 4/height
4090   # text with empty line in second screen
4091   s:text <- new [axy
4092 bxy
4093 cxy
4094 
4095 dxy
4096 exy
4097 fxy
4098 gxy
4099 ]
4100   e:&:editor <- new-editor s, 0/left, 4/right
4101   editor-render screen, e
4102   screen-should-contain [
4103     .          .
4104     .axy       .
4105     .bxy       .
4106     .cxy       .
4107   ]
4108   assume-console [
4109     press page-down
4110   ]
4111   run [
4112     editor-event-loop screen, console, e
4113   ]
4114   screen-should-contain [
4115     .          .
4116     .cxy       .
4117     .          .
4118     .dxy       .
4119   ]
4120   assume-console [
4121     press page-down
4122   ]
4123   run [
4124     editor-event-loop screen, console, e
4125   ]
4126   screen-should-contain [
4127     .          .
4128     .dxy       .
4129     .exy       .
4130     .fxy       .
4131   ]
4132   # scroll back up past empty line
4133   assume-console [
4134     press page-up
4135   ]
4136   run [
4137     editor-event-loop screen, console, e
4138   ]
4139   screen-should-contain [
4140     .          .
4141     .cxy       .
4142     .          .
4143     .dxy       .
4144   ]
4145 ]
4146 
4147 # ctrl-s - scroll up by one line
4148 # todo: scenarios
4149 
4150 after <handle-special-character> [
4151   {
4152     scroll-up?:bool <- equal c, 19/ctrl-s
4153     break-unless scroll-up?
4154     <begin-move-cursor>
4155     go-render?:bool, editor <- line-up editor, screen-height
4156     undo-coalesce-tag:num <- copy 5/line-up
4157     <end-move-cursor>
4158     return go-render?
4159   }
4160 ]
4161 
4162 def line-up editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
4163   local-scope
4164   load-inputs
4165   left:num <- get *editor, left:offset
4166   right:num <- get *editor, right:offset
4167   max:num <- subtract right, left
4168   old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4169   new-top:&:duplex-list:char <- before-start-of-next-line old-top, max
4170   movement?:bool <- not-equal old-top, new-top
4171   {
4172     break-unless movement?
4173     *editor <- put *editor, top-of-screen:offset, new-top
4174   }
4175   return movement?
4176 ]
4177 
4178 # ctrl-x - scroll down by one line
4179 # todo: scenarios
4180 
4181 after <handle-special-character> [
4182   {
4183     scroll-down?:bool <- equal c, 24/ctrl-x
4184     break-unless scroll-down?
4185     <begin-move-cursor>
4186     go-render?:bool, editor <- line-down editor, screen-height
4187     undo-coalesce-tag:num <- copy 6/line-down
4188     <end-move-cursor>
4189     return go-render?
4190   }
4191 ]
4192 
4193 def line-down editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
4194   local-scope
4195   load-inputs
4196   old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4197   new-top:&:duplex-list:char <- before-previous-screen-line old-top, editor
4198   movement?:bool <- not-equal old-top, new-top
4199   {
4200     break-unless movement?
4201     *editor <- put *editor, top-of-screen:offset, new-top
4202   }
4203   return movement?
4204 ]
4205 
4206 # ctrl-t - move current line to top of screen
4207 # todo: scenarios
4208 
4209 after <handle-special-character> [
4210   {
4211     scroll-down?:bool <- equal c, 20/ctrl-t
4212     break-unless scroll-down?
4213     <begin-move-cursor>
4214     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4215     cursor:&:duplex-list:char <- get *editor, before-cursor:offset
4216     cursor <- next cursor
4217     new-top:&:duplex-list:char <- before-previous-screen-line cursor, editor
4218     *editor <- put *editor, top-of-screen:offset, new-top
4219     *editor <- put *editor, cursor-row:offset, 1
4220     go-render?:bool <- not-equal new-top, old-top
4221     undo-coalesce-tag:num <- copy 0/never
4222     <end-move-cursor>
4223     return go-render?
4224   }
4225 ]
4226 
4227 # ctrl-/ - comment/uncomment current line
4228 
4229 after <handle-special-character> [
4230   {
4231     comment-toggle?:bool <- equal c, 31/ctrl-slash
4232     break-unless comment-toggle?
4233     cursor-column:num <- get *editor, cursor-column:offset
4234     data:&:duplex-list:char <- get *editor, data:offset
4235     <begin-insert-character>
4236     before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
4237     line-start:&:duplex-list:char <- next before-line-start
4238     commented-out?:bool <- match line-start, [#? ]  # comment prefix
4239     {
4240       break-unless commented-out?
4241       # uncomment
4242       data <- remove line-start, 3/length-comment-prefix, data
4243       cursor-column <- subtract cursor-column, 3/length-comment-prefix
4244       *editor <- put *editor, cursor-column:offset, cursor-column
4245       go-render? <- render-line-from-start screen, editor, 3/size-of-comment-leader
4246     }
4247     {
4248       break-if commented-out?
4249       # comment
4250       insert before-line-start, [#? ]
4251       cursor-column <- add cursor-column, 3/length-comment-prefix
4252       *editor <- put *editor, cursor-column:offset, cursor-column
4253       go-render? <- render-line-from-start screen, editor, 0
4254     }
4255     <end-insert-character>
4256     return
4257   }
4258 ]
4259 
4260 # Render just from the start of the current line, and only if it wasn't
4261 # wrapping before (include margin) and isn't wrapping now. Otherwise just tell
4262 # the caller to go-render? the entire screen.
4263 def render-line-from-start screen:&:screen, editor:&:editor, right-margin:num -> go-render?:bool, screen:&:screen [
4264   local-scope
4265   load-inputs
4266   before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
4267   line-start:&:duplex-list:char <- next before-line-start
4268   color:num <- copy 7/white
4269   left:num <- get *editor, left:offset
4270   cursor-row:num <- get *editor, cursor-row:offset
4271   screen <- move-cursor screen, cursor-row, left
4272   right:num <- get *editor, right:offset
4273   end:num <- subtract right, right-margin
4274   i:num <- copy 0
4275   curr:&:duplex-list:char <- copy line-start
4276   {
4277     render-all?:bool <- greater-or-equal i, end
4278     return-if render-all?, true/go-render
4279     break-unless curr
4280     c:char <- get *curr, value:offset
4281     newline?:bool <- equal c, 10/newline
4282     break-if newline?
4283     color <- get-color color, c
4284     print screen, c, color
4285     curr <- next curr
4286     i <- add i, 1
4287     loop
4288   }
4289   clear-line-until screen, right
4290   return false/dont-render
4291 ]
4292 
4293 def before-start-of-screen-line editor:&:editor -> result:&:duplex-list:char [
4294   local-scope
4295   load-inputs
4296   cursor:&:duplex-list:char <- get *editor, before-cursor:offset
4297   {
4298     next:&:duplex-list:char <- next cursor
4299     break-unless next
4300     cursor <- copy next
4301   }
4302   result <- before-previous-screen-line cursor, editor
4303 ]
4304 
4305 scenario editor-comments-empty-line [
4306   local-scope
4307   assume-screen 10/width, 5/height
4308   e:&:editor <- new-editor [], 0/left, 5/right
4309   editor-render screen, e
4310   $clear-trace
4311   assume-console [
4312     press ctrl-slash
4313   ]
4314   run [
4315     editor-event-loop screen, console, e
4316     4:num/raw <- get *e, cursor-row:offset
4317     5:num/raw <- get *e, cursor-column:offset
4318   ]
4319   screen-should-contain [
4320     .          .
4321     .#?        .
4322     .╌╌╌╌╌     .
4323     .          .
4324   ]
4325   memory-should-contain [
4326     4 <- 1
4327     5 <- 3
4328   ]
4329   check-trace-count-for-label 5, [print-character]
4330 ]
4331 
4332 scenario editor-comments-at-start-of-contents [
4333   local-scope
4334   assume-screen 10/width, 5/height
4335   e:&:editor <- new-editor [ab], 0/left, 10/right
4336   editor-render screen, e
4337   $clear-trace
4338   assume-console [
4339     press ctrl-slash
4340   ]
4341   run [
4342     editor-event-loop screen, console, e
4343     4:num/raw <- get *e, cursor-row:offset
4344     5:num/raw <- get *e, cursor-column:offset
4345   ]
4346   screen-should-contain [
4347     .          .
4348     .#? ab     .
4349     .╌╌╌╌╌╌╌╌╌╌.
4350     .          .
4351   ]
4352   memory-should-contain [
4353     4 <- 1
4354     5 <- 3
4355   ]
4356   check-trace-count-for-label 10, [print-character]
4357 ]
4358 
4359 scenario editor-comments-at-end-of-contents [
4360   local-scope
4361   assume-screen 10/width, 5/height
4362   e:&:editor <- new-editor [ab], 0/left, 10/right
4363   editor-render screen, e
4364   $clear-trace
4365   assume-console [
4366     left-click 1, 7
4367     press ctrl-slash
4368   ]
4369   run [
4370     editor-event-loop screen, console, e
4371     4:num/raw <- get *e, cursor-row:offset
4372     5:num/raw <- get *e, cursor-column:offset
4373   ]
4374   screen-should-contain [
4375     .          .
4376     .#? ab     .
4377     .╌╌╌╌╌╌╌╌╌╌.
4378     .          .
4379   ]
4380   memory-should-contain [
4381     4 <- 1
4382     5 <- 5
4383   ]
4384   check-trace-count-for-label 10, [print-character]
4385   # toggle to uncomment
4386   $clear-trace
4387   assume-console [
4388     press ctrl-slash
4389   ]
4390   run [
4391     editor-event-loop screen, console, e
4392     4:num/raw <- get *e, cursor-row:offset
4393     5:num/raw <- get *e, cursor-column:offset
4394   ]
4395   screen-should-contain [
4396     .          .
4397     .ab        .
4398     .╌╌╌╌╌╌╌╌╌╌.
4399     .          .
4400   ]
4401   check-trace-count-for-label 10, [print-character]
4402 ]
4403 
4404 scenario editor-comments-almost-wrapping-line [
4405   local-scope
4406   assume-screen 10/width, 5/height
4407   # editor starts out with a non-wrapping line
4408   e:&:editor <- new-editor [abcd], 0/left, 5/right
4409   editor-render screen, e
4410   screen-should-contain [
4411     .          .
4412     .abcd      .
4413     .╌╌╌╌╌     .
4414     .          .
4415   ]
4416   $clear-trace
4417   # on commenting the line is now wrapped
4418   assume-console [
4419     left-click 1, 7
4420     press ctrl-slash
4421   ]
4422   run [
4423     editor-event-loop screen, console, e
4424   ]
4425   screen-should-contain [
4426     .          .
4427     .#? a↩     .
4428     .bcd       .
4429     .╌╌╌╌╌     .
4430     .          .
4431   ]
4432 ]
4433 
4434 scenario editor-uncomments-just-wrapping-line [
4435   local-scope
4436   assume-screen 10/width, 5/height
4437   # editor starts out with a comment that wraps the line
4438   e:&:editor <- new-editor [#? ab], 0/left, 5/right
4439   editor-render screen, e
4440   screen-should-contain [
4441     .          .
4442     .#? a↩     .
4443     .b         .
4444     .╌╌╌╌╌     .
4445     .          .
4446   ]
4447   $clear-trace
4448   # on uncommenting the line is no longer wrapped
4449   assume-console [
4450     left-click 1, 7
4451     press ctrl-slash
4452   ]
4453   run [
4454     editor-event-loop screen, console, e
4455   ]
4456   screen-should-contain [
4457     .          .
4458     .ab        .
4459     .╌╌╌╌╌     .
4460     .          .
4461   ]
4462 ]