1 ## special shortcuts for manipulating the editor
   2 # Some keys on the keyboard generate unicode characters, others generate
   3 # terminfo key codes. We need to modify different places in the two cases.
   4 
   5 # tab - insert two spaces
   6 
   7 scenario editor-inserts-two-spaces-on-tab [
   8   local-scope
   9   assume-screen 10/width, 5/height
  10   s:text <- new [ab
  11 cd]
  12   e:&:editor <- new-editor s, 0/left, 5/right
  13   editor-render screen, e
  14   $clear-trace
  15   assume-console [
  16     press tab
  17   ]
  18   run [
  19     editor-event-loop screen, console, e
  20   ]
  21   screen-should-contain [
  22     .          .
  23     .  ab      .
  24     .cd        .
  25   ]
  26   # we render at most two editor rows worth (one row for each space)
  27   check-trace-count-for-label-lesser-than 10, [print-character]
  28 ]
  29 
  30 scenario editor-inserts-two-spaces-and-wraps-line-on-tab [
  31   local-scope
  32   assume-screen 10/width, 5/height
  33   e:&:editor <- new-editor [abcd], 0/left, 5/right
  34   editor-render screen, e
  35   $clear-trace
  36   assume-console [
  37     press tab
  38   ]
  39   run [
  40     editor-event-loop screen, console, e
  41   ]
  42   screen-should-contain [
  43     .          .
  44     .  ab↩     .
  45     .cd        .
  46   ]
  47   # we re-render the whole editor
  48   check-trace-count-for-label-greater-than 10, [print-character]
  49 ]
  50 
  51 after <handle-special-character> [
  52   {
  53     tab?:bool <- equal c, 9/tab
  54     break-unless tab?
  55     <begin-insert-character>
  56     # todo: decompose insert-at-cursor into editor update and screen update,
  57     # so that 'tab' doesn't render the current line multiple times
  58     insert-at-cursor editor, 32/space, screen
  59     go-render? <- insert-at-cursor editor, 32/space, screen
  60     <end-insert-character>
  61     return
  62   }
  63 ]
  64 
  65 # backspace - delete character before cursor
  66 
  67 scenario editor-handles-backspace-key [
  68   local-scope
  69   assume-screen 10/width, 5/height
  70   e:&:editor <- new-editor [abc], 0/left, 10/right
  71   editor-render screen, e
  72   $clear-trace
  73   assume-console [
  74     left-click 1, 1
  75     press backspace
  76   ]
  77   run [
  78     editor-event-loop screen, console, e
  79     4:num/raw <- get *e, cursor-row:offset
  80     5:num/raw <- get *e, cursor-column:offset
  81   ]
  82   screen-should-contain [
  83     .          .
  84     .bc        .
  85     .╌╌╌╌╌╌╌╌╌╌.
  86     .          .
  87   ]
  88   memory-should-contain [
  89     4 <- 1
  90     5 <- 0
  91   ]
  92   check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
  93 ]
  94 
  95 after <handle-special-character> [
  96   {
  97     delete-previous-character?:bool <- equal c, 8/backspace
  98     break-unless delete-previous-character?
  99     <begin-backspace-character>
 100     go-render?:bool, backspaced-cell:&:duplex-list:char <- delete-before-cursor editor, screen
 101     <end-backspace-character>
 102     return
 103   }
 104 ]
 105 
 106 # return values:
 107 #   go-render? - whether caller needs to update the screen
 108 #   backspaced-cell - value deleted (or 0 if nothing was deleted) so we can save it for undo, etc.
 109 def delete-before-cursor editor:&:editor, screen:&:screen -> go-render?:bool, backspaced-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
 110   local-scope
 111   load-inputs
 112   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 113   data:&:duplex-list:char <- get *editor, data:offset
 114   # if at start of text (before-cursor at § sentinel), return
 115   prev:&:duplex-list:char <- prev before-cursor
 116   return-unless prev, 0/no-more-render, 0/nothing-deleted
 117   trace 10, [app], [delete-before-cursor]
 118   original-row:num <- get *editor, cursor-row:offset
 119   scroll?:bool <- move-cursor-coordinates-left editor
 120   backspaced-cell:&:duplex-list:char <- copy before-cursor
 121   data <- remove before-cursor, data  # will also neatly trim next/prev pointers in backspaced-cell/before-cursor
 122   before-cursor <- copy prev
 123   *editor <- put *editor, before-cursor:offset, before-cursor
 124   return-if scroll?, 1/go-render
 125   screen-width:num <- screen-width screen
 126   cursor-row:num <- get *editor, cursor-row:offset
 127   cursor-column:num <- get *editor, cursor-column:offset
 128   # did we just backspace over a newline?
 129   same-row?:bool <- equal cursor-row, original-row
 130   return-unless same-row?, 1/go-render
 131   left:num <- get *editor, left:offset
 132   right:num <- get *editor, right:offset
 133   curr:&:duplex-list:char <- next before-cursor
 134   screen <- move-cursor screen, cursor-row, cursor-column
 135   curr-column:num <- copy cursor-column
 136   {
 137     # hit right margin? give up and let caller render
 138     at-right?:bool <- greater-or-equal curr-column, right
 139     return-if at-right?, 1/go-render
 140     break-unless curr
 141     # newline? done.
 142     currc:char <- get *curr, value:offset
 143     at-newline?:bool <- equal currc, 10/newline
 144     break-if at-newline?
 145     screen <- print screen, currc
 146     curr-column <- add curr-column, 1
 147     curr <- next curr
 148     loop
 149   }
 150   # we're guaranteed not to be at the right margin
 151   space:char <- copy 32/space
 152   screen <- print screen, space
 153   go-render? <- copy 0/false
 154 ]
 155 
 156 def move-cursor-coordinates-left editor:&:editor -> go-render?:bool, editor:&:editor [
 157   local-scope
 158   load-inputs
 159   go-render?:bool <- copy 0/false
 160   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 161   cursor-row:num <- get *editor, cursor-row:offset
 162   cursor-column:num <- get *editor, cursor-column:offset
 163   left:num <- get *editor, left:offset
 164   # if not at left margin, move one character left
 165   {
 166     at-left-margin?:bool <- equal cursor-column, left
 167     break-if at-left-margin?
 168     trace 10, [app], [decrementing cursor column]
 169     cursor-column <- subtract cursor-column, 1
 170     *editor <- put *editor, cursor-column:offset, cursor-column
 171     return
 172   }
 173   # if at left margin, we must move to previous row:
 174   top-of-screen?:bool <- equal cursor-row, 1  # exclude menu bar
 175   {
 176     break-if top-of-screen?
 177     cursor-row <- subtract cursor-row, 1
 178     *editor <- put *editor, cursor-row:offset, cursor-row
 179   }
 180   {
 181     break-unless top-of-screen?
 182     <scroll-up>
 183     go-render? <- copy 1/true
 184   }
 185   {
 186     # case 1: if previous character was newline, figure out how long the previous line is
 187     previous-character:char <- get *before-cursor, value:offset
 188     previous-character-is-newline?:bool <- equal previous-character, 10/newline
 189     break-unless previous-character-is-newline?
 190     # compute length of previous line
 191     trace 10, [app], [switching to previous line]
 192     d:&:duplex-list:char <- get *editor, data:offset
 193     end-of-line:num <- previous-line-length before-cursor, d
 194     right:num <- get *editor, right:offset
 195     width:num <- subtract right, left
 196     wrap?:bool <- greater-than end-of-line, width
 197     {
 198       break-unless wrap?
 199       _, column-offset:num <- divide-with-remainder end-of-line, width
 200       cursor-column <- add left, column-offset
 201       *editor <- put *editor, cursor-column:offset, cursor-column
 202     }
 203     {
 204       break-if wrap?
 205       cursor-column <- add left, end-of-line
 206       *editor <- put *editor, cursor-column:offset, cursor-column
 207     }
 208     return
 209   }
 210   # case 2: if previous-character was not newline, we're just at a wrapped line
 211   trace 10, [app], [wrapping to previous line]
 212   right:num <- get *editor, right:offset
 213   cursor-column <- subtract right, 1  # leave room for wrap icon
 214   *editor <- put *editor, cursor-column:offset, cursor-column
 215 ]
 216 
 217 # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
 218 # the length of the previous line before the 'curr' pointer.
 219 def previous-line-length curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
 220   local-scope
 221   load-inputs
 222   result:num <- copy 0
 223   return-unless curr
 224   at-start?:bool <- equal curr, start
 225   return-if at-start?
 226   {
 227     curr <- prev curr
 228     break-unless curr
 229     at-start?:bool <- equal curr, start
 230     break-if at-start?
 231     c:char <- get *curr, value:offset
 232     at-newline?:bool <- equal c, 10/newline
 233     break-if at-newline?
 234     result <- add result, 1
 235     loop
 236   }
 237 ]
 238 
 239 scenario editor-clears-last-line-on-backspace [
 240   local-scope
 241   assume-screen 10/width, 5/height
 242   s:text <- new [ab
 243 cd]
 244   e:&:editor <- new-editor s, 0/left, 10/right
 245   assume-console [
 246     left-click 2, 0
 247     press backspace
 248   ]
 249   run [
 250     editor-event-loop screen, console, e
 251     4:num/raw <- get *e, cursor-row:offset
 252     5:num/raw <- get *e, cursor-column:offset
 253   ]
 254   screen-should-contain [
 255     .          .
 256     .abcd      .
 257     .╌╌╌╌╌╌╌╌╌╌.
 258     .          .
 259   ]
 260   memory-should-contain [
 261     4 <- 1
 262     5 <- 2
 263   ]
 264 ]
 265 
 266 scenario editor-joins-and-wraps-lines-on-backspace [
 267   local-scope
 268   assume-screen 10/width, 5/height
 269   # initialize editor with two long-ish but non-wrapping lines
 270   s:text <- new [abc def
 271 ghi jkl]
 272   e:&:editor <- new-editor s, 0/left, 10/right
 273   editor-render screen, e
 274   $clear-trace
 275   # position the cursor at the start of the second and hit backspace
 276   assume-console [
 277     left-click 2, 0
 278     press backspace
 279   ]
 280   run [
 281     editor-event-loop screen, console, e
 282   ]
 283   # resulting single line should wrap correctly
 284   screen-should-contain [
 285     .          .
 286     .abc defgh↩.
 287     .i jkl     .
 288     .╌╌╌╌╌╌╌╌╌╌.
 289     .          .
 290   ]
 291 ]
 292 
 293 scenario editor-wraps-long-lines-on-backspace [
 294   local-scope
 295   assume-screen 10/width, 5/height
 296   # initialize editor in part of the screen with a long line
 297   e:&:editor <- new-editor [abc def ghij], 0/left, 8/right
 298   editor-render screen, e
 299   # confirm that it wraps
 300   screen-should-contain [
 301     .          .
 302     .abc def↩  .
 303     . ghij     .
 304     .╌╌╌╌╌╌╌╌  .
 305   ]
 306   $clear-trace
 307   # position the cursor somewhere in the middle of the top screen line and hit backspace
 308   assume-console [
 309     left-click 1, 4
 310     press backspace
 311   ]
 312   run [
 313     editor-event-loop screen, console, e
 314   ]
 315   # resulting single line should wrap correctly and not overflow its bounds
 316   screen-should-contain [
 317     .          .
 318     .abcdef ↩  .
 319     .ghij      .
 320     .╌╌╌╌╌╌╌╌  .
 321     .          .
 322   ]
 323 ]
 324 
 325 # delete - delete character at cursor
 326 
 327 scenario editor-handles-delete-key [
 328   local-scope
 329   assume-screen 10/width, 5/height
 330   e:&:editor <- new-editor [abc], 0/left, 10/right
 331   editor-render screen, e
 332   $clear-trace
 333   assume-console [
 334     press delete
 335   ]
 336   run [
 337     editor-event-loop screen, console, e
 338   ]
 339   screen-should-contain [
 340     .          .
 341     .bc        .
 342     .╌╌╌╌╌╌╌╌╌╌.
 343     .          .
 344   ]
 345   check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
 346   $clear-trace
 347   assume-console [
 348     press delete
 349   ]
 350   run [
 351     editor-event-loop screen, console, e
 352   ]
 353   screen-should-contain [
 354     .          .
 355     .c         .
 356     .╌╌╌╌╌╌╌╌╌╌.
 357     .          .
 358   ]
 359   check-trace-count-for-label 2, [print-character]  # new length to overwrite
 360 ]
 361 
 362 after <handle-special-key> [
 363   {
 364     delete-next-character?:bool <- equal k, 65522/delete
 365     break-unless delete-next-character?
 366     <begin-delete-character>
 367     go-render?:bool, deleted-cell:&:duplex-list:char <- delete-at-cursor editor, screen
 368     <end-delete-character>
 369     return
 370   }
 371 ]
 372 
 373 def delete-at-cursor editor:&:editor, screen:&:screen -> go-render?:bool, deleted-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
 374   local-scope
 375   load-inputs
 376   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 377   data:&:duplex-list:char <- get *editor, data:offset
 378   deleted-cell:&:duplex-list:char <- next before-cursor
 379   return-unless deleted-cell, 0/don't-render
 380   currc:char <- get *deleted-cell, value:offset
 381   data <- remove deleted-cell, data
 382   deleted-newline?:bool <- equal currc, 10/newline
 383   return-if deleted-newline?, 1/go-render
 384   # wasn't a newline? render rest of line
 385   curr:&:duplex-list:char <- next before-cursor  # refresh after remove above
 386   cursor-row:num <- get *editor, cursor-row:offset
 387   cursor-column:num <- get *editor, cursor-column:offset
 388   screen <- move-cursor screen, cursor-row, cursor-column
 389   curr-column:num <- copy cursor-column
 390   screen-width:num <- screen-width screen
 391   {
 392     # hit right margin? give up and let caller render
 393     at-right?:bool <- greater-or-equal curr-column, screen-width
 394     return-if at-right?, 1/go-render
 395     break-unless curr
 396     currc:char <- get *curr, value:offset
 397     at-newline?:bool <- equal currc, 10/newline
 398     break-if at-newline?
 399     screen <- print screen, currc
 400     curr-column <- add curr-column, 1
 401     curr <- next curr
 402     loop
 403   }
 404   # we're guaranteed not to be at the right margin
 405   space:char <- copy 32/space
 406   screen <- print screen, space
 407   go-render? <- copy 0/false
 408 ]
 409 
 410 # right arrow
 411 
 412 scenario editor-moves-cursor-right-with-key [
 413   local-scope
 414   assume-screen 10/width, 5/height
 415   e:&:editor <- new-editor [abc], 0/left, 10/right
 416   editor-render screen, e
 417   $clear-trace
 418   assume-console [
 419     press right-arrow
 420     type [0]
 421   ]
 422   run [
 423     editor-event-loop screen, console, e
 424   ]
 425   screen-should-contain [
 426     .          .
 427     .a0bc      .
 428     .╌╌╌╌╌╌╌╌╌╌.
 429     .          .
 430   ]
 431   check-trace-count-for-label 3, [print-character]  # 0 and following characters
 432 ]
 433 
 434 after <handle-special-key> [
 435   {
 436     move-to-next-character?:bool <- equal k, 65514/right-arrow
 437     break-unless move-to-next-character?
 438     # if not at end of text
 439     next-cursor:&:duplex-list:char <- next before-cursor
 440     break-unless next-cursor
 441     # scan to next character
 442     <begin-move-cursor>
 443     before-cursor <- copy next-cursor
 444     *editor <- put *editor, before-cursor:offset, before-cursor
 445     go-render?:bool <- move-cursor-coordinates-right editor, screen-height
 446     screen <- move-cursor screen, cursor-row, cursor-column
 447     undo-coalesce-tag:num <- copy 2/right-arrow
 448     <end-move-cursor>
 449     return
 450   }
 451 ]
 452 
 453 def move-cursor-coordinates-right editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
 454   local-scope
 455   load-inputs
 456   before-cursor:&:duplex-list:char <- get *editor before-cursor:offset
 457   cursor-row:num <- get *editor, cursor-row:offset
 458   cursor-column:num <- get *editor, cursor-column:offset
 459   left:num <- get *editor, left:offset
 460   right:num <- get *editor, right:offset
 461   # if crossed a newline, move cursor to start of next row
 462   {
 463     old-cursor-character:char <- get *before-cursor, value:offset
 464     was-at-newline?:bool <- equal old-cursor-character, 10/newline
 465     break-unless was-at-newline?
 466     cursor-row <- add cursor-row, 1
 467     *editor <- put *editor, cursor-row:offset, cursor-row
 468     cursor-column <- copy left
 469     *editor <- put *editor, cursor-column:offset, cursor-column
 470     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
 471     return-unless below-screen?, 0/don't-render
 472     <scroll-down>
 473     cursor-row <- subtract cursor-row, 1  # bring back into screen range
 474     *editor <- put *editor, cursor-row:offset, cursor-row
 475     return 1/go-render
 476   }
 477   # if the line wraps, move cursor to start of next row
 478   {
 479     # if we're at the column just before the wrap indicator
 480     wrap-column:num <- subtract right, 1
 481     at-wrap?:bool <- equal cursor-column, wrap-column
 482     break-unless at-wrap?
 483     # and if next character isn't newline
 484     next:&:duplex-list:char <- next before-cursor
 485     break-unless next
 486     next-character:char <- get *next, value:offset
 487     newline?:bool <- equal next-character, 10/newline
 488     break-if newline?
 489     cursor-row <- add cursor-row, 1
 490     *editor <- put *editor, cursor-row:offset, cursor-row
 491     cursor-column <- copy left
 492     *editor <- put *editor, cursor-column:offset, cursor-column
 493     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
 494     return-unless below-screen?, 0/no-more-render
 495     <scroll-down>
 496     cursor-row <- subtract cursor-row, 1  # bring back into screen range
 497     *editor <- put *editor, cursor-row:offset, cursor-row
 498     return 1/go-render
 499   }
 500   # otherwise move cursor one character right
 501   cursor-column <- add cursor-column, 1
 502   *editor <- put *editor, cursor-column:offset, cursor-column
 503   go-render? <- copy 0/false
 504 ]
 505 
 506 scenario editor-moves-cursor-to-next-line-with-right-arrow [
 507   local-scope
 508   assume-screen 10/width, 5/height
 509   s:text <- new [abc
 510 d]
 511   e:&:editor <- new-editor s, 0/left, 10/right
 512   editor-render screen, e
 513   $clear-trace
 514   # type right-arrow a few times to get to start of second line
 515   assume-console [
 516     press right-arrow
 517     press right-arrow
 518     press right-arrow
 519     press right-arrow  # next line
 520   ]
 521   run [
 522     editor-event-loop screen, console, e
 523   ]
 524   check-trace-count-for-label 0, [print-character]
 525   # type something and ensure it goes where it should
 526   assume-console [
 527     type [0]
 528   ]
 529   run [
 530     editor-event-loop screen, console, e
 531   ]
 532   screen-should-contain [
 533     .          .
 534     .abc       .
 535     .0d        .
 536     .╌╌╌╌╌╌╌╌╌╌.
 537     .          .
 538   ]
 539   check-trace-count-for-label 2, [print-character]  # new length of second line
 540 ]
 541 
 542 scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [
 543   local-scope
 544   assume-screen 10/width, 5/height
 545   s:text <- new [abc
 546 d]
 547   e:&:editor <- new-editor s, 1/left, 10/right
 548   editor-render screen, e
 549   assume-console [
 550     press right-arrow
 551     press right-arrow
 552     press right-arrow
 553     press right-arrow  # next line
 554     type [0]
 555   ]
 556   run [
 557     editor-event-loop screen, console, e
 558   ]
 559   screen-should-contain [
 560     .          .
 561     . abc      .
 562     . 0d       .
 563     . ╌╌╌╌╌╌╌╌╌.
 564     .          .
 565   ]
 566 ]
 567 
 568 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [
 569   local-scope
 570   assume-screen 10/width, 5/height
 571   e:&:editor <- new-editor [abcdef], 0/left, 5/right
 572   editor-render screen, e
 573   $clear-trace
 574   assume-console [
 575     left-click 1, 3
 576     press right-arrow
 577   ]
 578   run [
 579     editor-event-loop screen, console, e
 580     3:num/raw <- get *e, cursor-row:offset
 581     4:num/raw <- get *e, cursor-column:offset
 582   ]
 583   screen-should-contain [
 584     .          .
 585     .abcd↩     .
 586     .ef        .
 587     .╌╌╌╌╌     .
 588     .          .
 589   ]
 590   memory-should-contain [
 591     3 <- 2
 592     4 <- 0
 593   ]
 594   check-trace-count-for-label 0, [print-character]
 595 ]
 596 
 597 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [
 598   local-scope
 599   assume-screen 10/width, 5/height
 600   # line just barely wrapping
 601   e:&:editor <- new-editor [abcde], 0/left, 5/right
 602   editor-render screen, e
 603   $clear-trace
 604   # position cursor at last character before wrap and hit right-arrow
 605   assume-console [
 606     left-click 1, 3
 607     press right-arrow
 608   ]
 609   run [
 610     editor-event-loop screen, console, e
 611     3:num/raw <- get *e, cursor-row:offset
 612     4:num/raw <- get *e, cursor-column:offset
 613   ]
 614   memory-should-contain [
 615     3 <- 2
 616     4 <- 0
 617   ]
 618   # now hit right arrow again
 619   assume-console [
 620     press right-arrow
 621   ]
 622   run [
 623     editor-event-loop screen, console, e
 624     3:num/raw <- get *e, cursor-row:offset
 625     4:num/raw <- get *e, cursor-column:offset
 626   ]
 627   memory-should-contain [
 628     3 <- 2
 629     4 <- 1
 630   ]
 631   check-trace-count-for-label 0, [print-character]
 632 ]
 633 
 634 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [
 635   local-scope
 636   assume-screen 10/width, 5/height
 637   e:&:editor <- new-editor [abcdef], 1/left, 6/right
 638   editor-render screen, e
 639   $clear-trace
 640   assume-console [
 641     left-click 1, 4
 642     press right-arrow
 643   ]
 644   run [
 645     editor-event-loop screen, console, e
 646     3:num/raw <- get *e, cursor-row:offset
 647     4:num/raw <- get *e, cursor-column:offset
 648   ]
 649   screen-should-contain [
 650     .          .
 651     . abcd↩    .
 652     . ef       .
 653     . ╌╌╌╌╌    .
 654     .          .
 655   ]
 656   memory-should-contain [
 657     3 <- 2
 658     4 <- 1
 659   ]
 660   check-trace-count-for-label 0, [print-character]
 661 ]
 662 
 663 scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [
 664   local-scope
 665   assume-screen 10/width, 5/height
 666   s:text <- new [abc
 667 d]
 668   e:&:editor <- new-editor s, 0/left, 10/right
 669   editor-render screen, e
 670   $clear-trace
 671   # move to end of line, press right-arrow, type a character
 672   assume-console [
 673     left-click 1, 3
 674     press right-arrow
 675     type [0]
 676   ]
 677   run [
 678     editor-event-loop screen, console, e
 679   ]
 680   # new character should be in next line
 681   screen-should-contain [
 682     .          .
 683     .abc       .
 684     .0d        .
 685     .╌╌╌╌╌╌╌╌╌╌.
 686     .          .
 687   ]
 688   check-trace-count-for-label 2, [print-character]
 689 ]
 690 
 691 # todo: ctrl-right: next word-end
 692 
 693 # left arrow
 694 
 695 scenario editor-moves-cursor-left-with-key [
 696   local-scope
 697   assume-screen 10/width, 5/height
 698   e:&:editor <- new-editor [abc], 0/left, 10/right
 699   editor-render screen, e
 700   $clear-trace
 701   assume-console [
 702     left-click 1, 2
 703     press left-arrow
 704     type [0]
 705   ]
 706   run [
 707     editor-event-loop screen, console, e
 708   ]
 709   screen-should-contain [
 710     .          .
 711     .a0bc      .
 712     .╌╌╌╌╌╌╌╌╌╌.
 713     .          .
 714   ]
 715   check-trace-count-for-label 3, [print-character]
 716 ]
 717 
 718 after <handle-special-key> [
 719   {
 720     move-to-previous-character?:bool <- equal k, 65515/left-arrow
 721     break-unless move-to-previous-character?
 722     trace 10, [app], [left arrow]
 723     # if not at start of text (before-cursor at § sentinel)
 724     prev:&:duplex-list:char <- prev before-cursor
 725     return-unless prev, 0/don't-render
 726     <begin-move-cursor>
 727     go-render? <- move-cursor-coordinates-left editor
 728     before-cursor <- copy prev
 729     *editor <- put *editor, before-cursor:offset, before-cursor
 730     undo-coalesce-tag:num <- copy 1/left-arrow
 731     <end-move-cursor>
 732     return
 733   }
 734 ]
 735 
 736 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [
 737   local-scope
 738   assume-screen 10/width, 5/height
 739   # initialize editor with two lines
 740   s:text <- new [abc
 741 d]
 742   e:&:editor <- new-editor s, 0/left, 10/right
 743   editor-render screen, e
 744   $clear-trace
 745   # position cursor at start of second line (so there's no previous newline)
 746   assume-console [
 747     left-click 2, 0
 748     press left-arrow
 749   ]
 750   run [
 751     editor-event-loop screen, console, e
 752     3:num/raw <- get *e, cursor-row:offset
 753     4:num/raw <- get *e, cursor-column:offset
 754   ]
 755   memory-should-contain [
 756     3 <- 1
 757     4 <- 3
 758   ]
 759   check-trace-count-for-label 0, [print-character]
 760 ]
 761 
 762 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [
 763   local-scope
 764   assume-screen 10/width, 5/height
 765   # initialize editor with three lines
 766   s:text <- new [abc
 767 def
 768 g]
 769   e:&:editor <- new-editor s:text, 0/left, 10/right
 770   editor-render screen, e
 771   $clear-trace
 772   # position cursor further down (so there's a newline before the character at
 773   # the cursor)
 774   assume-console [
 775     left-click 3, 0
 776     press left-arrow
 777     type [0]
 778   ]
 779   run [
 780     editor-event-loop screen, console, e
 781   ]
 782   screen-should-contain [
 783     .          .
 784     .abc       .
 785     .def0      .
 786     .g         .
 787     .╌╌╌╌╌╌╌╌╌╌.
 788   ]
 789   check-trace-count-for-label 1, [print-character]  # just the '0'
 790 ]
 791 
 792 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [
 793   local-scope
 794   assume-screen 10/width, 5/height
 795   s:text <- new [abc
 796 def
 797 g]
 798   e:&:editor <- new-editor s, 0/left, 10/right
 799   editor-render screen, e
 800   $clear-trace
 801   # position cursor at start of text, press left-arrow, then type a character
 802   assume-console [
 803     left-click 1, 0
 804     press left-arrow
 805     type [0]
 806   ]
 807   run [
 808     editor-event-loop screen, console, e
 809   ]
 810   # left-arrow should have had no effect
 811   screen-should-contain [
 812     .          .
 813     .0abc      .
 814     .def       .
 815     .g         .
 816     .╌╌╌╌╌╌╌╌╌╌.
 817   ]
 818   check-trace-count-for-label 4, [print-character]  # length of first line
 819 ]
 820 
 821 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [
 822   local-scope
 823   assume-screen 10/width, 5/height
 824   # initialize editor with text containing an empty line
 825   s:text <- new [abc
 826 
 827 d]
 828   e:&:editor <- new-editor s, 0/left, 10/right
 829   editor-render screen, e:&:editor
 830   $clear-trace
 831   # position cursor right after empty line
 832   assume-console [
 833     left-click 3, 0
 834     press left-arrow
 835     type [0]
 836   ]
 837   run [
 838     editor-event-loop screen, console, e
 839   ]
 840   screen-should-contain [
 841     .          .
 842     .abc       .
 843     .0         .
 844     .d         .
 845     .╌╌╌╌╌╌╌╌╌╌.
 846   ]
 847   check-trace-count-for-label 1, [print-character]  # just the '0'
 848 ]
 849 
 850 scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [
 851   local-scope
 852   assume-screen 10/width, 5/height
 853   # initialize editor with a wrapping line
 854   e:&:editor <- new-editor [abcdef], 0/left, 5/right
 855   editor-render screen, e
 856   $clear-trace
 857   screen-should-contain [
 858     .          .
 859     .abcd↩     .
 860     .ef        .
 861     .╌╌╌╌╌     .
 862     .          .
 863   ]
 864   # position cursor right after empty line
 865   assume-console [
 866     left-click 2, 0
 867     press left-arrow
 868   ]
 869   run [
 870     editor-event-loop screen, console, e
 871     3:num/raw <- get *e, cursor-row:offset
 872     4:num/raw <- get *e, cursor-column:offset
 873   ]
 874   memory-should-contain [
 875     3 <- 1  # previous row
 876     4 <- 3  # right margin except wrap icon
 877   ]
 878   check-trace-count-for-label 0, [print-character]
 879 ]
 880 
 881 scenario editor-moves-across-screen-lines-to-wrapping-line-with-left-arrow [
 882   local-scope
 883   assume-screen 10/width, 5/height
 884   # initialize editor with a wrapping line followed by a second line
 885   s:text <- new [abcdef
 886 g]
 887   e:&:editor <- new-editor s, 0/left, 5/right
 888   editor-render screen, e
 889   $clear-trace
 890   screen-should-contain [
 891     .          .
 892     .abcd↩     .
 893     .ef        .
 894     .g         .
 895     .╌╌╌╌╌     .
 896   ]
 897   # position cursor right after empty line
 898   assume-console [
 899     left-click 3, 0
 900     press left-arrow
 901   ]
 902   run [
 903     editor-event-loop screen, console, e
 904     3:num/raw <- get *e, cursor-row:offset
 905     4:num/raw <- get *e, cursor-column:offset
 906   ]
 907   memory-should-contain [
 908     3 <- 2  # previous row
 909     4 <- 2  # end of wrapped line
 910   ]
 911   check-trace-count-for-label 0, [print-character]
 912 ]
 913 
 914 scenario editor-moves-across-screen-lines-to-non-wrapping-line-with-left-arrow [
 915   local-scope
 916   assume-screen 10/width, 5/height
 917   # initialize editor with a line on the verge of wrapping, followed by a second line
 918   s:text <- new [abcd
 919 e]
 920   e:&:editor <- new-editor s, 0/left, 5/right
 921   editor-render screen, e
 922   $clear-trace
 923   screen-should-contain [
 924     .          .
 925     .abcd      .
 926     .e         .
 927     .╌╌╌╌╌     .
 928     .          .
 929   ]
 930   # position cursor right after empty line
 931   assume-console [
 932     left-click 2, 0
 933     press left-arrow
 934   ]
 935   run [
 936     editor-event-loop screen, console, e
 937     3:num/raw <- get *e, cursor-row:offset
 938     4:num/raw <- get *e, cursor-column:offset
 939   ]
 940   memory-should-contain [
 941     3 <- 1  # previous row
 942     4 <- 4  # end of wrapped line
 943   ]
 944   check-trace-count-for-label 0, [print-character]
 945 ]
 946 
 947 # todo: ctrl-left: previous word-start
 948 
 949 # up arrow
 950 
 951 scenario editor-moves-to-previous-line-with-up-arrow [
 952   local-scope
 953   assume-screen 10/width, 5/height
 954   s:text <- new [abc
 955 def]
 956   e:&:editor <- new-editor s, 0/left, 10/right
 957   editor-render screen, e
 958   $clear-trace
 959   assume-console [
 960     left-click 2, 1
 961     press up-arrow
 962   ]
 963   run [
 964     editor-event-loop screen, console, e
 965     3:num/raw <- get *e, cursor-row:offset
 966     4:num/raw <- get *e, cursor-column:offset
 967   ]
 968   memory-should-contain [
 969     3 <- 1
 970     4 <- 1
 971   ]
 972   check-trace-count-for-label 0, [print-character]
 973   assume-console [
 974     type [0]
 975   ]
 976   run [
 977     editor-event-loop screen, console, e
 978   ]
 979   screen-should-contain [
 980     .          .
 981     .a0bc      .
 982     .def       .
 983     .╌╌╌╌╌╌╌╌╌╌.
 984     .          .
 985   ]
 986 ]
 987 
 988 after <handle-special-key> [
 989   {
 990     move-to-previous-line?:bool <- equal k, 65517/up-arrow
 991     break-unless move-to-previous-line?
 992     <begin-move-cursor>
 993     go-render? <- move-to-previous-line editor
 994     undo-coalesce-tag:num <- copy 3/up-arrow
 995     <end-move-cursor>
 996     return
 997   }
 998 ]
 999 
1000 def move-to-previous-line editor:&:editor -> go-render?:bool, editor:&:editor [
1001   local-scope
1002   load-inputs
1003   go-render?:bool <- copy 0/false
1004   cursor-row:num <- get *editor, cursor-row:offset
1005   cursor-column:num <- get *editor, cursor-column:offset
1006   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1007   left:num <- get *editor, left:offset
1008   right:num <- get *editor, right:offset
1009   already-at-top?:bool <- lesser-or-equal cursor-row, 1/top
1010   {
1011     # if cursor not at top, move it
1012     break-if already-at-top?
1013     # if not at start of screen line, move to start of screen line (previous newline)
1014     # then scan back another line
1015     # if either step fails, give up without modifying cursor or coordinates
1016     curr:&:duplex-list:char <- copy before-cursor
1017     old:&:duplex-list:char <- copy curr
1018     {
1019       at-left?:bool <- equal cursor-column, left
1020       break-if at-left?
1021       curr <- before-previous-screen-line curr, editor
1022       no-motion?:bool <- equal curr, old
1023       return-if no-motion?
1024     }
1025     {
1026       curr <- before-previous-screen-line curr, editor
1027       no-motion?:bool <- equal curr, old
1028       return-if no-motion?
1029     }
1030     before-cursor <- copy curr
1031     *editor <- put *editor, before-cursor:offset, before-cursor
1032     cursor-row <- subtract cursor-row, 1
1033     *editor <- put *editor, cursor-row:offset, cursor-row
1034     # scan ahead to right column or until end of line
1035     target-column:num <- copy cursor-column
1036     cursor-column <- copy left
1037     *editor <- put *editor, cursor-column:offset, cursor-column
1038     {
1039       done?:bool <- greater-or-equal cursor-column, target-column
1040       break-if done?
1041       curr:&:duplex-list:char <- next before-cursor
1042       break-unless curr
1043       currc:char <- get *curr, value:offset
1044       at-newline?:bool <- equal currc, 10/newline
1045       break-if at-newline?
1046       #
1047       before-cursor <- copy curr
1048       *editor <- put *editor, before-cursor:offset, before-cursor
1049       cursor-column <- add cursor-column, 1
1050       *editor <- put *editor, cursor-column:offset, cursor-column
1051       loop
1052     }
1053     return
1054   }
1055   {
1056     # if cursor already at top, scroll up
1057     break-unless already-at-top?
1058     <scroll-up>
1059     return 1/go-render
1060   }
1061 ]
1062 
1063 # 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 0/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 0/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 0/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 0/don't-render
1480   }
1481   +try-to-scroll
1482   <scroll-down>
1483   go-render? <- copy 1/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 0/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 0/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 0/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 0/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?, 1/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?, 1/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 0/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 0/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, 0/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?, 1/go-render
2608   clear-line-until screen, right
2609   return 0/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, 0/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?, 0/don't-render
2843 ]
2844 
2845 # Takes a pointer into the doubly-linked list, scans ahead at most 'max'
2846 # positions until the next newline.
2847 # Returns original if no next newline.
2848 # Beware: never return null pointer.
2849 def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [
2850   local-scope
2851   load-inputs
2852   count:num <- copy 0
2853   curr:&:duplex-list:char <- copy original
2854   # skip the initial newline if it exists
2855   {
2856     c:char <- get *curr, value:offset
2857     at-newline?:bool <- equal c, 10/newline
2858     break-unless at-newline?
2859     curr <- next curr
2860     count <- add count, 1
2861   }
2862   {
2863     return-unless curr, original
2864     done?:bool <- greater-or-equal count, max
2865     break-if done?
2866     c:char <- get *curr, value:offset
2867     at-newline?:bool <- equal c, 10/newline
2868     break-if at-newline?
2869     curr <- next curr
2870     count <- add count, 1
2871     loop
2872   }
2873   return-unless curr, original
2874   return curr
2875 ]
2876 
2877 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys [
2878   local-scope
2879   # screen has 1 line for menu + 3 lines
2880   assume-screen 10/width, 4/height
2881   # initialize editor with a long, wrapped line and more than a screen of
2882   # other lines
2883   s:text <- new [abcdef
2884 g
2885 h
2886 i]
2887   e:&:editor <- new-editor s, 0/left, 5/right
2888   editor-render screen, e
2889   screen-should-contain [
2890     .          .
2891     .abcd↩     .
2892     .ef        .
2893     .g         .
2894   ]
2895   # position cursor at last line, then try to move further down
2896   assume-console [
2897     left-click 3, 0
2898     press down-arrow
2899   ]
2900   run [
2901     editor-event-loop screen, console, e
2902   ]
2903   # screen shows partial wrapped line
2904   screen-should-contain [
2905     .          .
2906     .ef        .
2907     .g         .
2908     .h         .
2909   ]
2910 ]
2911 
2912 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys-2 [
2913   local-scope
2914   # screen has 1 line for menu + 3 lines
2915   assume-screen 10/width, 4/height
2916   # editor starts with a long line wrapping twice
2917   s:text <- new [abcdefghij
2918 k
2919 l
2920 m]
2921   e:&:editor <- new-editor s, 0/left, 5/right
2922   # position cursor at last line, then try to move further down
2923   assume-console [
2924     left-click 3, 0
2925     press down-arrow
2926   ]
2927   run [
2928     editor-event-loop screen, console, e
2929   ]
2930   # screen shows partial wrapped line containing a wrap icon
2931   screen-should-contain [
2932     .          .
2933     .efgh↩     .
2934     .ij        .
2935     .k         .
2936   ]
2937   # scroll down again
2938   assume-console [
2939     press down-arrow
2940   ]
2941   run [
2942     editor-event-loop screen, console, e
2943   ]
2944   # screen shows partial wrapped line
2945   screen-should-contain [
2946     .          .
2947     .ij        .
2948     .k         .
2949     .l         .
2950   ]
2951 ]
2952 
2953 scenario editor-scrolls-down-when-line-wraps [
2954   local-scope
2955   # screen has 1 line for menu + 3 lines
2956   assume-screen 5/width, 4/height
2957   # editor contains a long line in the third line
2958   s:text <- new [a
2959 b
2960 cdef]
2961   e:&:editor <- new-editor s, 0/left, 5/right
2962   # position cursor at end, type a character
2963   assume-console [
2964     left-click 3, 4
2965     type [g]
2966   ]
2967   run [
2968     editor-event-loop screen, console, e
2969     3:num/raw <- get *e, cursor-row:offset
2970     4:num/raw <- get *e, cursor-column:offset
2971   ]
2972   # screen scrolls
2973   screen-should-contain [
2974     .     .
2975     .b    .
2976     .cdef↩.
2977     .g    .
2978   ]
2979   memory-should-contain [
2980     3 <- 3
2981     4 <- 1
2982   ]
2983 ]
2984 
2985 scenario editor-stops-scrolling-once-bottom-is-visible [
2986   local-scope
2987   # screen has 1 line for menu + 3 lines
2988   assume-screen 10/width, 4/height
2989   # initialize editor with 2 lines
2990   s:text <- new [a
2991 b]
2992   e:&:editor <- new-editor s, 0/left, 10/right
2993   editor-render screen, e
2994   screen-should-contain [
2995     .          .
2996     .a         .
2997     .b         .
2998     .╌╌╌╌╌╌╌╌╌╌.
2999   ]
3000   # position cursor at last line, then try to move further down
3001   assume-console [
3002     left-click 3, 0
3003     press down-arrow
3004   ]
3005   run [
3006     editor-event-loop screen, console, e
3007   ]
3008   # no change since the bottom border was already visible
3009   screen-should-contain [
3010     .          .
3011     .a         .
3012     .b         .
3013     .╌╌╌╌╌╌╌╌╌╌.
3014   ]
3015 ]
3016 
3017 scenario editor-scrolls-down-on-newline [
3018   local-scope
3019   assume-screen 5/width, 4/height
3020   # position cursor after last line and type newline
3021   s:text <- new [a
3022 b
3023 c]
3024   e:&:editor <- new-editor s, 0/left, 5/right
3025   assume-console [
3026     left-click 3, 4
3027     type [
3028 ]
3029   ]
3030   run [
3031     editor-event-loop screen, console, e
3032     3:num/raw <- get *e, cursor-row:offset
3033     4:num/raw <- get *e, cursor-column:offset
3034   ]
3035   # screen scrolls
3036   screen-should-contain [
3037     .     .
3038     .b    .
3039     .c    .
3040     .     .
3041   ]
3042   memory-should-contain [
3043     3 <- 3
3044     4 <- 0
3045   ]
3046 ]
3047 
3048 scenario editor-scrolls-down-on-right-arrow [
3049   local-scope
3050   # screen has 1 line for menu + 3 lines
3051   assume-screen 5/width, 4/height
3052   # editor contains a wrapped line
3053   s:text <- new [a
3054 b
3055 cdefgh]
3056   e:&:editor <- new-editor s, 0/left, 5/right
3057   # position cursor at end of screen and try to move right
3058   assume-console [
3059     left-click 3, 3
3060     press right-arrow
3061   ]
3062   run [
3063     editor-event-loop screen, console, e
3064     3:num/raw <- get *e, cursor-row:offset
3065     4:num/raw <- get *e, cursor-column:offset
3066   ]
3067   # screen scrolls
3068   screen-should-contain [
3069     .     .
3070     .b    .
3071     .cdef↩.
3072     .gh   .
3073   ]
3074   memory-should-contain [
3075     3 <- 3
3076     4 <- 0
3077   ]
3078 ]
3079 
3080 scenario editor-scrolls-down-on-right-arrow-2 [
3081   local-scope
3082   # screen has 1 line for menu + 3 lines
3083   assume-screen 5/width, 4/height
3084   # editor contains more lines than can fit on screen
3085   s:text <- new [a
3086 b
3087 c
3088 d]
3089   e:&:editor <- new-editor s, 0/left, 5/right
3090   # position cursor at end of screen and try to move right
3091   assume-console [
3092     left-click 3, 3
3093     press right-arrow
3094   ]
3095   run [
3096     editor-event-loop screen, console, e
3097     3:num/raw <- get *e, cursor-row:offset
3098     4:num/raw <- get *e, cursor-column:offset
3099   ]
3100   # screen scrolls
3101   screen-should-contain [
3102     .     .
3103     .b    .
3104     .c    .
3105     .d    .
3106   ]
3107   memory-should-contain [
3108     3 <- 3
3109     4 <- 0
3110   ]
3111 ]
3112 
3113 scenario editor-scrolls-at-end-on-down-arrow [
3114   local-scope
3115   assume-screen 10/width, 5/height
3116   s:text <- new [abc
3117 de]
3118   e:&:editor <- new-editor s, 0/left, 10/right
3119   editor-render screen, e
3120   $clear-trace
3121   # try to move down past end of text
3122   assume-console [
3123     left-click 2, 0
3124     press down-arrow
3125   ]
3126   run [
3127     editor-event-loop screen, console, e
3128     3:num/raw <- get *e, cursor-row:offset
3129     4:num/raw <- get *e, cursor-column:offset
3130   ]
3131   # no change
3132   memory-should-contain [
3133     3 <- 2
3134     4 <- 0
3135   ]
3136 ]
3137 
3138 scenario editor-combines-page-and-line-scroll [
3139   local-scope
3140   # screen has 1 line for menu + 3 lines
3141   assume-screen 10/width, 4/height
3142   # initialize editor with a few pages of lines
3143   s:text <- new [a
3144 b
3145 c
3146 d
3147 e
3148 f
3149 g]
3150   e:&:editor <- new-editor s, 0/left, 5/right
3151   editor-render screen, e
3152   # scroll down one page and one line
3153   assume-console [
3154     press page-down
3155     left-click 3, 0
3156     press down-arrow
3157   ]
3158   run [
3159     editor-event-loop screen, console, e
3160   ]
3161   # screen scrolls down 3 lines
3162   screen-should-contain [
3163     .          .
3164     .d         .
3165     .e         .
3166     .f         .
3167   ]
3168 ]
3169 
3170 # scroll up if necessary
3171 
3172 scenario editor-can-scroll-up-using-arrow-keys [
3173   local-scope
3174   # screen has 1 line for menu + 3 lines
3175   assume-screen 10/width, 4/height
3176   # initialize editor with >3 lines
3177   s:text <- new [a
3178 b
3179 c
3180 d]
3181   e:&:editor <- new-editor s, 0/left, 10/right
3182   editor-render screen, e
3183   screen-should-contain [
3184     .          .
3185     .a         .
3186     .b         .
3187     .c         .
3188   ]
3189   # position cursor at top of second page, then try to move up
3190   assume-console [
3191     press page-down
3192     press up-arrow
3193   ]
3194   run [
3195     editor-event-loop screen, console, e
3196   ]
3197   # screen slides by one line
3198   screen-should-contain [
3199     .          .
3200     .b         .
3201     .c         .
3202     .d         .
3203   ]
3204 ]
3205 
3206 after <scroll-up> [
3207   trace 10, [app], [scroll up]
3208   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3209   old-top:&:duplex-list:char <- copy top-of-screen
3210   top-of-screen <- before-previous-screen-line top-of-screen, editor
3211   *editor <- put *editor, top-of-screen:offset, top-of-screen
3212   no-movement?:bool <- equal old-top, top-of-screen
3213   return-if no-movement?, 0/don't-render
3214 ]
3215 
3216 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys [
3217   local-scope
3218   # screen has 1 line for menu + 3 lines
3219   assume-screen 10/width, 4/height
3220   # initialize editor with a long, wrapped line and more than a screen of
3221   # other lines
3222   s:text <- new [abcdef
3223 g
3224 h
3225 i]
3226   e:&:editor <- new-editor s, 0/left, 5/right
3227   editor-render screen, e
3228   screen-should-contain [
3229     .          .
3230     .abcd↩     .
3231     .ef        .
3232     .g         .
3233   ]
3234   # position cursor at top of second page, just below wrapped line
3235   assume-console [
3236     press page-down
3237   ]
3238   run [
3239     editor-event-loop screen, console, e
3240   ]
3241   screen-should-contain [
3242     .          .
3243     .g         .
3244     .h         .
3245     .i         .
3246   ]
3247   # now move up one line
3248   assume-console [
3249     press up-arrow
3250   ]
3251   run [
3252     editor-event-loop screen, console, e
3253   ]
3254   # screen shows partial wrapped line
3255   screen-should-contain [
3256     .          .
3257     .ef        .
3258     .g         .
3259     .h         .
3260   ]
3261 ]
3262 
3263 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-2 [
3264   local-scope
3265   # screen has 1 line for menu + 4 lines
3266   assume-screen 10/width, 5/height
3267   # editor starts with a long line wrapping twice, occupying 3 of the 4 lines
3268   s:text <- new [abcdefghij
3269 k
3270 l
3271 m]
3272   e:&:editor <- new-editor s, 0/left, 5/right
3273   editor-render screen, e
3274   # position cursor at top of second page
3275   assume-console [
3276     press page-down
3277   ]
3278   run [
3279     editor-event-loop screen, console, e
3280   ]
3281   screen-should-contain [
3282     .          .
3283     .k         .
3284     .l         .
3285     .m         .
3286     .╌╌╌╌╌     .
3287   ]
3288   # move up one line
3289   assume-console [
3290     press up-arrow
3291   ]
3292   run [
3293     editor-event-loop screen, console, e
3294   ]
3295   # screen shows partial wrapped line
3296   screen-should-contain [
3297     .          .
3298     .ij        .
3299     .k         .
3300     .l         .
3301     .m         .
3302   ]
3303   # move up a second line
3304   assume-console [
3305     press up-arrow
3306   ]
3307   run [
3308     editor-event-loop screen, console, e
3309   ]
3310   # screen shows partial wrapped line
3311   screen-should-contain [
3312     .          .
3313     .efgh↩     .
3314     .ij        .
3315     .k         .
3316     .l         .
3317   ]
3318   # move up a third line
3319   assume-console [
3320     press up-arrow
3321   ]
3322   run [
3323     editor-event-loop screen, console, e
3324   ]
3325   # screen shows partial wrapped line
3326   screen-should-contain [
3327     .          .
3328     .abcd↩     .
3329     .efgh↩     .
3330     .ij        .
3331     .k         .
3332   ]
3333 ]
3334 
3335 # same as editor-scrolls-up-past-wrapped-line-using-arrow-keys but length
3336 # slightly off, just to prevent over-training
3337 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-3 [
3338   local-scope
3339   # screen has 1 line for menu + 3 lines
3340   assume-screen 10/width, 4/height
3341   # initialize editor with a long, wrapped line and more than a screen of
3342   # other lines
3343   s:text <- new [abcdef
3344 g
3345 h
3346 i]
3347   e:&:editor <- new-editor s, 0/left, 6/right
3348   editor-render screen, e
3349   screen-should-contain [
3350     .          .
3351     .abcde↩    .
3352     .f         .
3353     .g         .
3354   ]
3355   # position cursor at top of second page, just below wrapped line
3356   assume-console [
3357     press page-down
3358   ]
3359   run [
3360     editor-event-loop screen, console, e
3361   ]
3362   screen-should-contain [
3363     .          .
3364     .g         .
3365     .h         .
3366     .i         .
3367   ]
3368   # now move up one line
3369   assume-console [
3370     press up-arrow
3371   ]
3372   run [
3373     editor-event-loop screen, console, e
3374   ]
3375   # screen shows partial wrapped line
3376   screen-should-contain [
3377     .          .
3378     .f         .
3379     .g         .
3380     .h         .
3381   ]
3382 ]
3383 
3384 # check empty lines
3385 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-4 [
3386   local-scope
3387   assume-screen 10/width, 4/height
3388   # initialize editor with some lines around an empty line
3389   s:text <- new [a
3390 b
3391 
3392 c
3393 d
3394 e]
3395   e:&:editor <- new-editor s, 0/left, 6/right
3396   editor-render screen, e
3397   assume-console [
3398     press page-down
3399   ]
3400   run [
3401     editor-event-loop screen, console, e
3402   ]
3403   screen-should-contain [
3404     .          .
3405     .          .
3406     .c         .
3407     .d         .
3408   ]
3409   assume-console [
3410     press page-down
3411   ]
3412   run [
3413     editor-event-loop screen, console, e
3414   ]
3415   screen-should-contain [
3416     .          .
3417     .d         .
3418     .e         .
3419     .╌╌╌╌╌╌    .
3420   ]
3421   assume-console [
3422     press page-up
3423   ]
3424   run [
3425     editor-event-loop screen, console, e
3426   ]
3427   screen-should-contain [
3428     .          .
3429     .          .
3430     .c         .
3431     .d         .
3432   ]
3433 ]
3434 
3435 scenario editor-scrolls-up-on-left-arrow [
3436   local-scope
3437   # screen has 1 line for menu + 3 lines
3438   assume-screen 5/width, 4/height
3439   # editor contains >3 lines
3440   s:text <- new [a
3441 b
3442 c
3443 d
3444 e]
3445   e:&:editor <- new-editor s, 0/left, 5/right
3446   editor-render screen, e
3447   # position cursor at top of second page
3448   assume-console [
3449     press page-down
3450   ]
3451   run [
3452     editor-event-loop screen, console, e
3453   ]
3454   screen-should-contain [
3455     .     .
3456     .c    .
3457     .d    .
3458     .e    .
3459   ]
3460   # now try to move left
3461   assume-console [
3462     press left-arrow
3463   ]
3464   run [
3465     editor-event-loop screen, console, e
3466     3:num/raw <- get *e, cursor-row:offset
3467     4:num/raw <- get *e, cursor-column:offset
3468   ]
3469   # screen scrolls
3470   screen-should-contain [
3471     .     .
3472     .b    .
3473     .c    .
3474     .d    .
3475   ]
3476   memory-should-contain [
3477     3 <- 1
3478     4 <- 1
3479   ]
3480 ]
3481 
3482 scenario editor-can-scroll-up-to-start-of-file [
3483   local-scope
3484   # screen has 1 line for menu + 3 lines
3485   assume-screen 10/width, 4/height
3486   # initialize editor with >3 lines
3487   s:text <- new [a
3488 b
3489 c
3490 d]
3491   e:&:editor <- new-editor s, 0/left, 10/right
3492   editor-render screen, e
3493   screen-should-contain [
3494     .          .
3495     .a         .
3496     .b         .
3497     .c         .
3498   ]
3499   # position cursor at top of second page, then try to move up to start of
3500   # text
3501   assume-console [
3502     press page-down
3503     press up-arrow
3504     press up-arrow
3505   ]
3506   run [
3507     editor-event-loop screen, console, e
3508   ]
3509   # screen slides by one line
3510   screen-should-contain [
3511     .          .
3512     .a         .
3513     .b         .
3514     .c         .
3515   ]
3516   # try to move up again
3517   assume-console [
3518     press up-arrow
3519   ]
3520   run [
3521     editor-event-loop screen, console, e
3522   ]
3523   # screen remains unchanged
3524   screen-should-contain [
3525     .          .
3526     .a         .
3527     .b         .
3528     .c         .
3529   ]
3530 ]
3531 
3532 # ctrl-f/page-down - render next page if it exists
3533 
3534 scenario editor-can-scroll [
3535   local-scope
3536   assume-screen 10/width, 4/height
3537   s:text <- new [a
3538 b
3539 c
3540 d]
3541   e:&:editor <- new-editor s, 0/left, 10/right
3542   editor-render screen, e
3543   screen-should-contain [
3544     .          .
3545     .a         .
3546     .b         .
3547     .c         .
3548   ]
3549   # scroll down
3550   assume-console [
3551     press page-down
3552   ]
3553   run [
3554     editor-event-loop screen, console, e
3555   ]
3556   # screen shows next page
3557   screen-should-contain [
3558     .          .
3559     .c         .
3560     .d         .
3561     .╌╌╌╌╌╌╌╌╌╌.
3562   ]
3563 ]
3564 
3565 after <handle-special-character> [
3566   {
3567     page-down?:bool <- equal c, 6/ctrl-f
3568     break-unless page-down?
3569     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3570     <begin-move-cursor>
3571     page-down editor
3572     undo-coalesce-tag:num <- copy 0/never
3573     <end-move-cursor>
3574     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3575     movement?:bool <- not-equal top-of-screen, old-top
3576     return movement?/go-render
3577   }
3578 ]
3579 
3580 after <handle-special-key> [
3581   {
3582     page-down?:bool <- equal k, 65518/page-down
3583     break-unless page-down?
3584     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3585     <begin-move-cursor>
3586     page-down editor
3587     undo-coalesce-tag:num <- copy 0/never
3588     <end-move-cursor>
3589     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3590     movement?:bool <- not-equal top-of-screen, old-top
3591     return movement?/go-render
3592   }
3593 ]
3594 
3595 # page-down skips entire wrapped lines, so it can't scroll past lines
3596 # taking up the entire screen
3597 def page-down editor:&:editor -> editor:&:editor [
3598   local-scope
3599   load-inputs
3600   # if editor contents don't overflow screen, do nothing
3601   bottom-of-screen:&:duplex-list:char <- get *editor, bottom-of-screen:offset
3602   return-unless bottom-of-screen
3603   # if not, position cursor at final character
3604   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
3605   before-cursor:&:duplex-list:char <- prev bottom-of-screen
3606   *editor <- put *editor, before-cursor:offset, before-cursor
3607   # keep one line in common with previous page
3608   {
3609     last:char <- get *before-cursor, value:offset
3610     newline?:bool <- equal last, 10/newline
3611     break-unless newline?:bool
3612     before-cursor <- prev before-cursor
3613     *editor <- put *editor, before-cursor:offset, before-cursor
3614   }
3615   # move cursor and top-of-screen to start of that line
3616   move-to-start-of-line editor
3617   before-cursor <- get *editor, before-cursor:offset
3618   *editor <- put *editor, top-of-screen:offset, before-cursor
3619 ]
3620 
3621 # jump to previous newline
3622 def move-to-start-of-line editor:&:editor -> editor:&:editor [
3623   local-scope
3624   load-inputs
3625   # update cursor column
3626   left:num <- get *editor, left:offset
3627   cursor-column:num <- copy left
3628   *editor <- put *editor, cursor-column:offset, cursor-column
3629   # update before-cursor
3630   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
3631   init:&:duplex-list:char <- get *editor, data:offset
3632   # while not at start of line, move
3633   {
3634     at-start-of-text?:bool <- equal before-cursor, init
3635     break-if at-start-of-text?
3636     prev:char <- get *before-cursor, value:offset
3637     at-start-of-line?:bool <- equal prev, 10/newline
3638     break-if at-start-of-line?
3639     before-cursor <- prev before-cursor
3640     *editor <- put *editor, before-cursor:offset, before-cursor
3641     assert before-cursor, [move-to-start-of-line tried to move before start of text]
3642     loop
3643   }
3644 ]
3645 
3646 scenario editor-does-not-scroll-past-end [
3647   local-scope
3648   assume-screen 10/width, 4/height
3649   s:text <- new [a
3650 b]
3651   e:&:editor <- new-editor s, 0/left, 10/right
3652   editor-render screen, e
3653   screen-should-contain [
3654     .          .
3655     .a         .
3656     .b         .
3657     .╌╌╌╌╌╌╌╌╌╌.
3658   ]
3659   # scroll down
3660   assume-console [
3661     press page-down
3662   ]
3663   run [
3664     editor-event-loop screen, console, e
3665   ]
3666   # screen remains unmodified
3667   screen-should-contain [
3668     .          .
3669     .a         .
3670     .b         .
3671     .╌╌╌╌╌╌╌╌╌╌.
3672   ]
3673 ]
3674 
3675 scenario editor-starts-next-page-at-start-of-wrapped-line [
3676   local-scope
3677   # screen has 1 line for menu + 3 lines for text
3678   assume-screen 10/width, 4/height
3679   # editor contains a long last line
3680   s:text <- new [a
3681 b
3682 cdefgh]
3683   # editor screen triggers wrap of last line
3684   e:&:editor <- new-editor s, 0/left, 4/right
3685   editor-render screen, e
3686   # some part of last line is not displayed
3687   screen-should-contain [
3688     .          .
3689     .a         .
3690     .b         .
3691     .cde↩      .
3692   ]
3693   # scroll down
3694   assume-console [
3695     press page-down
3696   ]
3697   run [
3698     editor-event-loop screen, console, e
3699   ]
3700   # screen shows entire wrapped line
3701   screen-should-contain [
3702     .          .
3703     .cde↩      .
3704     .fgh       .
3705     .╌╌╌╌      .
3706   ]
3707 ]
3708 
3709 scenario editor-starts-next-page-at-start-of-wrapped-line-2 [
3710   local-scope
3711   # screen has 1 line for menu + 3 lines for text
3712   assume-screen 10/width, 4/height
3713   # editor contains a very long line that occupies last two lines of screen
3714   # and still has something left over
3715   s:text <- new [a
3716 bcdefgh]
3717   e:&:editor <- new-editor s, 0/left, 4/right
3718   editor-render screen, e
3719   # some part of last line is not displayed
3720   screen-should-contain [
3721     .          .
3722     .a         .
3723     .bcd↩      .
3724     .efg↩      .
3725   ]
3726   # scroll down
3727   assume-console [
3728     press page-down
3729   ]
3730   run [
3731     editor-event-loop screen, console, e
3732   ]
3733   # screen shows entire wrapped line
3734   screen-should-contain [
3735     .          .
3736     .bcd↩      .
3737     .efg↩      .
3738     .h         .
3739   ]
3740 ]
3741 
3742 # ctrl-b/page-up - render previous page if it exists
3743 
3744 scenario editor-can-scroll-up [
3745   local-scope
3746   assume-screen 10/width, 4/height
3747   s:text <- new [a
3748 b
3749 c
3750 d]
3751   e:&:editor <- new-editor s, 0/left, 10/right
3752   editor-render screen, e
3753   screen-should-contain [
3754     .          .
3755     .a         .
3756     .b         .
3757     .c         .
3758   ]
3759   # scroll down
3760   assume-console [
3761     press page-down
3762   ]
3763   run [
3764     editor-event-loop screen, console, e
3765   ]
3766   # screen shows next page
3767   screen-should-contain [
3768     .          .
3769     .c         .
3770     .d         .
3771     .╌╌╌╌╌╌╌╌╌╌.
3772   ]
3773   # scroll back up
3774   assume-console [
3775     press page-up
3776   ]
3777   run [
3778     editor-event-loop screen, console, e
3779   ]
3780   # screen shows original page again
3781   screen-should-contain [
3782     .          .
3783     .a         .
3784     .b         .
3785     .c         .
3786   ]
3787 ]
3788 
3789 after <handle-special-character> [
3790   {
3791     page-up?:bool <- equal c, 2/ctrl-b
3792     break-unless page-up?
3793     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3794     <begin-move-cursor>
3795     editor <- page-up editor, screen-height
3796     undo-coalesce-tag:num <- copy 0/never
3797     <end-move-cursor>
3798     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3799     movement?:bool <- not-equal top-of-screen, old-top
3800     return movement?/go-render
3801   }
3802 ]
3803 
3804 after <handle-special-key> [
3805   {
3806     page-up?:bool <- equal k, 65519/page-up
3807     break-unless page-up?
3808     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
3809     <begin-move-cursor>
3810     editor <- page-up editor, screen-height
3811     undo-coalesce-tag:num <- copy 0/never
3812     <end-move-cursor>
3813     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3814     movement?:bool <- not-equal top-of-screen, old-top
3815     # don't bother re-rendering if nothing changed. todo: test this
3816     return movement?/go-render
3817   }
3818 ]
3819 
3820 def page-up editor:&:editor, screen-height:num -> editor:&:editor [
3821   local-scope
3822   load-inputs
3823   max:num <- subtract screen-height, 1/menu-bar, 1/overlapping-line
3824   count:num <- copy 0
3825   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
3826   {
3827     done?:bool <- greater-or-equal count, max
3828     break-if done?
3829     prev:&:duplex-list:char <- before-previous-screen-line top-of-screen, editor
3830     break-unless prev
3831     top-of-screen <- copy prev
3832     *editor <- put *editor, top-of-screen:offset, top-of-screen
3833     count <- add count, 1
3834     loop
3835   }
3836 ]
3837 
3838 scenario editor-can-scroll-up-multiple-pages [
3839   local-scope
3840   # screen has 1 line for menu + 3 lines
3841   assume-screen 10/width, 4/height
3842   # initialize editor with 8 lines
3843   s:text <- new [a
3844 b
3845 c
3846 d
3847 e
3848 f
3849 g
3850 h]
3851   e:&:editor <- new-editor s, 0/left, 10/right
3852   editor-render screen, e
3853   screen-should-contain [
3854     .          .
3855     .a         .
3856     .b         .
3857     .c         .
3858   ]
3859   # scroll down two pages
3860   assume-console [
3861     press page-down
3862     press page-down
3863   ]
3864   run [
3865     editor-event-loop screen, console, e
3866   ]
3867   # screen shows third page
3868   screen-should-contain [
3869     .          .
3870     .e         .
3871     .f         .
3872     .g         .
3873   ]
3874   # scroll up
3875   assume-console [
3876     press page-up
3877   ]
3878   run [
3879     editor-event-loop screen, console, e
3880   ]
3881   # screen shows second page
3882   screen-should-contain [
3883     .          .
3884     .c         .
3885     .d         .
3886     .e         .
3887   ]
3888   # scroll up again
3889   assume-console [
3890     press page-up
3891   ]
3892   run [
3893     editor-event-loop screen, console, e
3894   ]
3895   # screen shows original page again
3896   screen-should-contain [
3897     .          .
3898     .a         .
3899     .b         .
3900     .c         .
3901   ]
3902 ]
3903 
3904 scenario editor-can-scroll-up-wrapped-lines [
3905   local-scope
3906   # screen has 1 line for menu + 5 lines for text
3907   assume-screen 10/width, 6/height
3908   # editor contains a long line in the first page
3909   s:text <- new [a
3910 b
3911 cdefgh
3912 i
3913 j
3914 k
3915 l
3916 m
3917 n
3918 o]
3919   # editor screen triggers wrap of last line
3920   e:&:editor <- new-editor s, 0/left, 4/right
3921   editor-render screen, e
3922   # some part of last line is not displayed
3923   screen-should-contain [
3924     .          .
3925     .a         .
3926     .b         .
3927     .cde↩      .
3928     .fgh       .
3929     .i         .
3930   ]
3931   # scroll down a page and a line
3932   assume-console [
3933     press page-down
3934     left-click 5, 0
3935     press down-arrow
3936   ]
3937   run [
3938     editor-event-loop screen, console, e
3939   ]
3940   # screen shows entire wrapped line
3941   screen-should-contain [
3942     .          .
3943     .j         .
3944     .k         .
3945     .l         .
3946     .m         .
3947     .n         .
3948   ]
3949   # now scroll up one page
3950   assume-console [
3951     press page-up
3952   ]
3953   run [
3954     editor-event-loop screen, console, e
3955   ]
3956   # screen resets
3957   screen-should-contain [
3958     .          .
3959     .b         .
3960     .cde↩      .
3961     .fgh       .
3962     .i         .
3963     .j         .
3964   ]
3965 ]
3966 
3967 scenario editor-can-scroll-up-wrapped-lines-2 [
3968   local-scope
3969   # screen has 1 line for menu + 3 lines for text
3970   assume-screen 10/width, 4/height
3971   # editor contains a very long line that occupies last two lines of screen
3972   # and still has something left over
3973   s:text <- new [a
3974 bcdefgh]
3975   e:&:editor <- new-editor s, 0/left, 4/right
3976   editor-render screen, e
3977   # some part of last line is not displayed
3978   screen-should-contain [
3979     .          .
3980     .a         .
3981     .bcd↩      .
3982     .efg↩      .
3983   ]
3984   # scroll down
3985   assume-console [
3986     press page-down
3987   ]
3988   run [
3989     editor-event-loop screen, console, e
3990   ]
3991   # screen shows entire wrapped line
3992   screen-should-contain [
3993     .          .
3994     .bcd↩      .
3995     .efg↩      .
3996     .h         .
3997   ]
3998   # scroll back up
3999   assume-console [
4000     press page-up
4001   ]
4002   run [
4003     editor-event-loop screen, console, e
4004   ]
4005   # screen resets
4006   screen-should-contain [
4007     .          .
4008     .a         .
4009     .bcd↩      .
4010     .efg↩      .
4011   ]
4012 ]
4013 
4014 scenario editor-can-scroll-up-past-nonempty-lines [
4015   local-scope
4016   assume-screen 10/width, 4/height
4017   # text with empty line in second screen
4018   s:text <- new [axx
4019 bxx
4020 cxx
4021 dxx
4022 exx
4023 fxx
4024 gxx
4025 hxx
4026 ]
4027   e:&:editor <- new-editor s, 0/left, 4/right
4028   editor-render screen, e
4029   screen-should-contain [
4030     .          .
4031     .axx       .
4032     .bxx       .
4033     .cxx       .
4034   ]
4035   assume-console [
4036     press page-down
4037   ]
4038   run [
4039     editor-event-loop screen, console, e
4040   ]
4041   screen-should-contain [
4042     .          .
4043     .cxx       .
4044     .dxx       .
4045     .exx       .
4046   ]
4047   assume-console [
4048     press page-down
4049   ]
4050   run [
4051     editor-event-loop screen, console, e
4052   ]
4053   screen-should-contain [
4054     .          .
4055     .exx       .
4056     .fxx       .
4057     .gxx       .
4058   ]
4059   # scroll back up past empty line
4060   assume-console [
4061     press page-up
4062   ]
4063   run [
4064     editor-event-loop screen, console, e
4065   ]
4066   screen-should-contain [
4067     .          .
4068     .cxx       .
4069     .dxx       .
4070     .exx       .
4071   ]
4072 ]
4073 
4074 scenario editor-can-scroll-up-past-empty-lines [
4075   local-scope
4076   assume-screen 10/width, 4/height
4077   # text with empty line in second screen
4078   s:text <- new [axy
4079 bxy
4080 cxy
4081 
4082 dxy
4083 exy
4084 fxy
4085 gxy
4086 ]
4087   e:&:editor <- new-editor s, 0/left, 4/right
4088   editor-render screen, e
4089   screen-should-contain [
4090     .          .
4091     .axy       .
4092     .bxy       .
4093     .cxy       .
4094   ]
4095   assume-console [
4096     press page-down
4097   ]
4098   run [
4099     editor-event-loop screen, console, e
4100   ]
4101   screen-should-contain [
4102     .          .
4103     .cxy       .
4104     .          .
4105     .dxy       .
4106   ]
4107   assume-console [
4108     press page-down
4109   ]
4110   run [
4111     editor-event-loop screen, console, e
4112   ]
4113   screen-should-contain [
4114     .          .
4115     .dxy       .
4116     .exy       .
4117     .fxy       .
4118   ]
4119   # scroll back up past empty line
4120   assume-console [
4121     press page-up
4122   ]
4123   run [
4124     editor-event-loop screen, console, e
4125   ]
4126   screen-should-contain [
4127     .          .
4128     .cxy       .
4129     .          .
4130     .dxy       .
4131   ]
4132 ]
4133 
4134 # ctrl-s - scroll up by one line
4135 # todo: scenarios
4136 
4137 after <handle-special-character> [
4138   {
4139     scroll-up?:bool <- equal c, 19/ctrl-s
4140     break-unless scroll-up?
4141     <begin-move-cursor>
4142     go-render?:bool, editor <- line-up editor, screen-height
4143     undo-coalesce-tag:num <- copy 5/line-up
4144     <end-move-cursor>
4145     return go-render?
4146   }
4147 ]
4148 
4149 def line-up editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
4150   local-scope
4151   load-inputs
4152   left:num <- get *editor, left:offset
4153   right:num <- get *editor, right:offset
4154   max:num <- subtract right, left
4155   old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4156   new-top:&:duplex-list:char <- before-start-of-next-line old-top, max
4157   movement?:bool <- not-equal old-top, new-top
4158   {
4159     break-unless movement?
4160     *editor <- put *editor, top-of-screen:offset, new-top
4161   }
4162   return movement?
4163 ]
4164 
4165 # ctrl-x - scroll down by one line
4166 # todo: scenarios
4167 
4168 after <handle-special-character> [
4169   {
4170     scroll-down?:bool <- equal c, 24/ctrl-x
4171     break-unless scroll-down?
4172     <begin-move-cursor>
4173     go-render?:bool, editor <- line-down editor, screen-height
4174     undo-coalesce-tag:num <- copy 6/line-down
4175     <end-move-cursor>
4176     return go-render?
4177   }
4178 ]
4179 
4180 def line-down editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
4181   local-scope
4182   load-inputs
4183   old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4184   new-top:&:duplex-list:char <- before-previous-screen-line old-top, editor
4185   movement?:bool <- not-equal old-top, new-top
4186   {
4187     break-unless movement?
4188     *editor <- put *editor, top-of-screen:offset, new-top
4189   }
4190   return movement?
4191 ]
4192 
4193 # ctrl-t - move current line to top of screen
4194 # todo: scenarios
4195 
4196 after <handle-special-character> [
4197   {
4198     scroll-down?:bool <- equal c, 20/ctrl-t
4199     break-unless scroll-down?
4200     <begin-move-cursor>
4201     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
4202     cursor:&:duplex-list:char <- get *editor, before-cursor:offset
4203     cursor <- next cursor
4204     new-top:&:duplex-list:char <- before-previous-screen-line cursor, editor
4205     *editor <- put *editor, top-of-screen:offset, new-top
4206     *editor <- put *editor, cursor-row:offset, 1
4207     go-render?:bool <- not-equal new-top, old-top
4208     undo-coalesce-tag:num <- copy 0/never
4209     <end-move-cursor>
4210     return go-render?
4211   }
4212 ]
4213 
4214 # ctrl-/ - comment/uncomment current line
4215 
4216 after <handle-special-character> [
4217   {
4218     comment-toggle?:bool <- equal c, 31/ctrl-slash
4219     break-unless comment-toggle?
4220     cursor-column:num <- get *editor, cursor-column:offset
4221     data:&:duplex-list:char <- get *editor, data:offset
4222     <begin-insert-character>
4223     before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
4224     line-start:&:duplex-list:char <- next before-line-start
4225     commented-out?:bool <- match line-start, [#? ]  # comment prefix
4226     {
4227       break-unless commented-out?
4228       # uncomment
4229       data <- remove line-start, 3/length-comment-prefix, data
4230       cursor-column <- subtract cursor-column, 3/length-comment-prefix
4231       *editor <- put *editor, cursor-column:offset, cursor-column
4232       go-render? <- render-line-from-start screen, editor, 3/size-of-comment-leader
4233     }
4234     {
4235       break-if commented-out?
4236       # comment
4237       insert before-line-start, [#? ]
4238       cursor-column <- add cursor-column, 3/length-comment-prefix
4239       *editor <- put *editor, cursor-column:offset, cursor-column
4240       go-render? <- render-line-from-start screen, editor, 0
4241     }
4242     <end-insert-character>
4243     return
4244   }
4245 ]
4246 
4247 # Render just from the start of the current line, and only if it wasn't
4248 # wrapping before (include margin) and isn't wrapping now. Otherwise just tell
4249 # the caller to go-render? the entire screen.
4250 def render-line-from-start screen:&:screen, editor:&:editor, right-margin:num -> go-render?:bool, screen:&:screen [
4251   local-scope
4252   load-inputs
4253   before-line-start:&:duplex-list:char <- before-start-of-screen-line editor
4254   line-start:&:duplex-list:char <- next before-line-start
4255   color:num <- copy 7/white
4256   left:num <- get *editor, left:offset
4257   cursor-row:num <- get *editor, cursor-row:offset
4258   screen <- move-cursor screen, cursor-row, left
4259   right:num <- get *editor, right:offset
4260   end:num <- subtract right, right-margin
4261   i:num <- copy 0
4262   curr:&:duplex-list:char <- copy line-start
4263   {
4264     render-all?:bool <- greater-or-equal i, end
4265     return-if render-all?, 1/go-render
4266     break-unless curr
4267     c:char <- get *curr, value:offset
4268     newline?:bool <- equal c, 10/newline
4269     break-if newline?
4270     color <- get-color color, c
4271     print screen, c, color
4272     curr <- next curr
4273     i <- add i, 1
4274     loop
4275   }
4276   clear-line-until screen, right
4277   return 0/dont-render
4278 ]
4279 
4280 def before-start-of-screen-line editor:&:editor -> result:&:duplex-list:char [
4281   local-scope
4282   load-inputs
4283   cursor:&:duplex-list:char <- get *editor, before-cursor:offset
4284   {
4285     next:&:duplex-list:char <- next cursor
4286     break-unless next
4287     cursor <- copy next
4288   }
4289   result <- before-previous-screen-line cursor, editor
4290 ]
4291 
4292 scenario editor-comments-empty-line [
4293   local-scope
4294   assume-screen 10/width, 5/height
4295   e:&:editor <- new-editor [], 0/left, 5/right
4296   editor-render screen, e
4297   $clear-trace
4298   assume-console [
4299     press ctrl-slash
4300   ]
4301   run [
4302     editor-event-loop screen, console, e
4303     4:num/raw <- get *e, cursor-row:offset
4304     5:num/raw <- get *e, cursor-column:offset
4305   ]
4306   screen-should-contain [
4307     .          .
4308     .#?        .
4309     .╌╌╌╌╌     .
4310     .          .
4311   ]
4312   memory-should-contain [
4313     4 <- 1
4314     5 <- 3
4315   ]
4316   check-trace-count-for-label 5, [print-character]
4317 ]
4318 
4319 scenario editor-comments-at-start-of-contents [
4320   local-scope
4321   assume-screen 10/width, 5/height
4322   e:&:editor <- new-editor [ab], 0/left, 10/right
4323   editor-render screen, e
4324   $clear-trace
4325   assume-console [
4326     press ctrl-slash
4327   ]
4328   run [
4329     editor-event-loop screen, console, e
4330     4:num/raw <- get *e, cursor-row:offset
4331     5:num/raw <- get *e, cursor-column:offset
4332   ]
4333   screen-should-contain [
4334     .          .
4335     .#? ab     .
4336     .╌╌╌╌╌╌╌╌╌╌.
4337     .          .
4338   ]
4339   memory-should-contain [
4340     4 <- 1
4341     5 <- 3
4342   ]
4343   check-trace-count-for-label 10, [print-character]
4344 ]
4345 
4346 scenario editor-comments-at-end-of-contents [
4347   local-scope
4348   assume-screen 10/width, 5/height
4349   e:&:editor <- new-editor [ab], 0/left, 10/right
4350   editor-render screen, e
4351   $clear-trace
4352   assume-console [
4353     left-click 1, 7
4354     press ctrl-slash
4355   ]
4356   run [
4357     editor-event-loop screen, console, e
4358     4:num/raw <- get *e, cursor-row:offset
4359     5:num/raw <- get *e, cursor-column:offset
4360   ]
4361   screen-should-contain [
4362     .          .
4363     .#? ab     .
4364     .╌╌╌╌╌╌╌╌╌╌.
4365     .          .
4366   ]
4367   memory-should-contain [
4368     4 <- 1
4369     5 <- 5
4370   ]
4371   check-trace-count-for-label 10, [print-character]
4372   # toggle to uncomment
4373   $clear-trace
4374   assume-console [
4375     press ctrl-slash
4376   ]
4377   run [
4378     editor-event-loop screen, console, e
4379     4:num/raw <- get *e, cursor-row:offset
4380     5:num/raw <- get *e, cursor-column:offset
4381   ]
4382   screen-should-contain [
4383     .          .
4384     .ab        .
4385     .╌╌╌╌╌╌╌╌╌╌.
4386     .          .
4387   ]
4388   check-trace-count-for-label 10, [print-character]
4389 ]
4390 
4391 scenario editor-comments-almost-wrapping-line [
4392   local-scope
4393   assume-screen 10/width, 5/height
4394   # editor starts out with a non-wrapping line
4395   e:&:editor <- new-editor [abcd], 0/left, 5/right
4396   editor-render screen, e
4397   screen-should-contain [
4398     .          .
4399     .abcd      .
4400     .╌╌╌╌╌     .
4401     .          .
4402   ]
4403   $clear-trace
4404   # on commenting the line is now wrapped
4405   assume-console [
4406     left-click 1, 7
4407     press ctrl-slash
4408   ]
4409   run [
4410     editor-event-loop screen, console, e
4411   ]
4412   screen-should-contain [
4413     .          .
4414     .#? a↩     .
4415     .bcd       .
4416     .╌╌╌╌╌     .
4417     .          .
4418   ]
4419 ]
4420 
4421 scenario editor-uncomments-just-wrapping-line [
4422   local-scope
4423   assume-screen 10/width, 5/height
4424   # editor starts out with a comment that wraps the line
4425   e:&:editor <- new-editor [#? ab], 0/left, 5/right
4426   editor-render screen, e
4427   screen-should-contain [
4428     .          .
4429     .#? a↩     .
4430     .b         .
4431     .╌╌╌╌╌     .
4432     .          .
4433   ]
4434   $clear-trace
4435   # on uncommenting the line is no longer wrapped
4436   assume-console [
4437     left-click 1, 7
4438     press ctrl-slash
4439   ]
4440   run [
4441     editor-event-loop screen, console, e
4442   ]
4443   screen-should-contain [
4444     .          .
4445     .ab        .
4446     .╌╌╌╌╌     .
4447     .          .
4448   ]
4449 ]