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   # just one character in final line
  11   s:text <- new [ab
  12 cd]
  13   e:&:editor <- new-editor s, 0/left, 5/right
  14   assume-console [
  15     press tab
  16   ]
  17   run [
  18     editor-event-loop screen, console, e
  19   ]
  20   screen-should-contain [
  21     .          .
  22     .  ab      .
  23     .cd        .
  24   ]
  25 ]
  26 
  27 after <handle-special-character> [
  28   {
  29     tab?:bool <- equal c, 9/tab
  30     break-unless tab?
  31     <insert-character-begin>
  32     insert-at-cursor editor, 32/space, screen
  33     insert-at-cursor editor, 32/space, screen
  34     <insert-character-end>
  35     return 1/go-render
  36   }
  37 ]
  38 
  39 # backspace - delete character before cursor
  40 
  41 scenario editor-handles-backspace-key [
  42   local-scope
  43   assume-screen 10/width, 5/height
  44   e:&:editor <- new-editor [abc], 0/left, 10/right
  45   editor-render screen, e
  46   $clear-trace
  47   assume-console [
  48     left-click 1, 1
  49     press backspace
  50   ]
  51   run [
  52     editor-event-loop screen, console, e
  53     4:num/raw <- get *e, cursor-row:offset
  54     5:num/raw <- get *e, cursor-column:offset
  55   ]
  56   screen-should-contain [
  57     .          .
  58     .bc        .
  59     .╌╌╌╌╌╌╌╌╌╌.
  60     .          .
  61   ]
  62   memory-should-contain [
  63     4 <- 1
  64     5 <- 0
  65   ]
  66   check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
  67 ]
  68 
  69 after <handle-special-character> [
  70   {
  71     delete-previous-character?:bool <- equal c, 8/backspace
  72     break-unless delete-previous-character?
  73     <backspace-character-begin>
  74     go-render?:bool, backspaced-cell:&:duplex-list:char <- delete-before-cursor editor, screen
  75     <backspace-character-end>
  76     return
  77   }
  78 ]
  79 
  80 # return values:
  81 #   go-render? - whether caller needs to update the screen
  82 #   backspaced-cell - value deleted (or 0 if nothing was deleted) so we can save it for undo, etc.
  83 def delete-before-cursor editor:&:editor, screen:&:screen -> go-render?:bool, backspaced-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
  84   local-scope
  85   load-ingredients
  86   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
  87   data:&:duplex-list:char <- get *editor, data:offset
  88   # if at start of text (before-cursor at § sentinel), return
  89   prev:&:duplex-list:char <- prev before-cursor
  90   return-unless prev, 0/no-more-render, 0/nothing-deleted
  91   trace 10, [app], [delete-before-cursor]
  92   original-row:num <- get *editor, cursor-row:offset
  93   scroll?:bool <- move-cursor-coordinates-left editor
  94   backspaced-cell:&:duplex-list:char <- copy before-cursor
  95   data <- remove before-cursor, data  # will also neatly trim next/prev pointers in backspaced-cell/before-cursor
  96   before-cursor <- copy prev
  97   *editor <- put *editor, before-cursor:offset, before-cursor
  98   return-if scroll?, 1/go-render
  99   screen-width:num <- screen-width screen
 100   cursor-row:num <- get *editor, cursor-row:offset
 101   cursor-column:num <- get *editor, cursor-column:offset
 102   # did we just backspace over a newline?
 103   same-row?:bool <- equal cursor-row, original-row
 104   return-unless same-row?, 1/go-render
 105   left:num <- get *editor, left:offset
 106   right:num <- get *editor, right:offset
 107   curr:&:duplex-list:char <- next before-cursor
 108   screen <- move-cursor screen, cursor-row, cursor-column
 109   curr-column:num <- copy cursor-column
 110   {
 111     # hit right margin? give up and let caller render
 112     at-right?:bool <- greater-or-equal curr-column, right
 113     return-if at-right?, 1/go-render
 114     break-unless curr
 115     # newline? done.
 116     currc:char <- get *curr, value:offset
 117     at-newline?:bool <- equal currc, 10/newline
 118     break-if at-newline?
 119     screen <- print screen, currc
 120     curr-column <- add curr-column, 1
 121     curr <- next curr
 122     loop
 123   }
 124   # we're guaranteed not to be at the right margin
 125   space:char <- copy 32/space
 126   screen <- print screen, space
 127   go-render? <- copy 0/false
 128 ]
 129 
 130 def move-cursor-coordinates-left editor:&:editor -> go-render?:bool, editor:&:editor [
 131   local-scope
 132   load-ingredients
 133   go-render?:bool <- copy 0/false
 134   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 135   cursor-row:num <- get *editor, cursor-row:offset
 136   cursor-column:num <- get *editor, cursor-column:offset
 137   left:num <- get *editor, left:offset
 138   # if not at left margin, move one character left
 139   {
 140     at-left-margin?:bool <- equal cursor-column, left
 141     break-if at-left-margin?
 142     trace 10, [app], [decrementing cursor column]
 143     cursor-column <- subtract cursor-column, 1
 144     *editor <- put *editor, cursor-column:offset, cursor-column
 145     return
 146   }
 147   # if at left margin, we must move to previous row:
 148   top-of-screen?:bool <- equal cursor-row, 1  # exclude menu bar
 149   {
 150     break-if top-of-screen?
 151     cursor-row <- subtract cursor-row, 1
 152     *editor <- put *editor, cursor-row:offset, cursor-row
 153   }
 154   {
 155     break-unless top-of-screen?
 156     <scroll-up>
 157     go-render? <- copy 1/true
 158   }
 159   {
 160     # case 1: if previous character was newline, figure out how long the previous line is
 161     previous-character:char <- get *before-cursor, value:offset
 162     previous-character-is-newline?:bool <- equal previous-character, 10/newline
 163     break-unless previous-character-is-newline?
 164     # compute length of previous line
 165     trace 10, [app], [switching to previous line]
 166     d:&:duplex-list:char <- get *editor, data:offset
 167     end-of-line:num <- previous-line-length before-cursor, d
 168     right:num <- get *editor, right:offset
 169     width:num <- subtract right, left
 170     wrap?:bool <- greater-than end-of-line, width
 171     {
 172       break-unless wrap?
 173       _, column-offset:num <- divide-with-remainder end-of-line, width
 174       cursor-column <- add left, column-offset
 175       *editor <- put *editor, cursor-column:offset, cursor-column
 176     }
 177     {
 178       break-if wrap?
 179       cursor-column <- add left, end-of-line
 180       *editor <- put *editor, cursor-column:offset, cursor-column
 181     }
 182     return
 183   }
 184   # case 2: if previous-character was not newline, we're just at a wrapped line
 185   trace 10, [app], [wrapping to previous line]
 186   right:num <- get *editor, right:offset
 187   cursor-column <- subtract right, 1  # leave room for wrap icon
 188   *editor <- put *editor, cursor-column:offset, cursor-column
 189 ]
 190 
 191 # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
 192 # the length of the previous line before the 'curr' pointer.
 193 def previous-line-length curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
 194   local-scope
 195   load-ingredients
 196   result:num <- copy 0
 197   return-unless curr
 198   at-start?:bool <- equal curr, start
 199   return-if at-start?
 200   {
 201     curr <- prev curr
 202     break-unless curr
 203     at-start?:bool <- equal curr, start
 204     break-if at-start?
 205     c:char <- get *curr, value:offset
 206     at-newline?:bool <- equal c, 10/newline
 207     break-if at-newline?
 208     result <- add result, 1
 209     loop
 210   }
 211 ]
 212 
 213 scenario editor-clears-last-line-on-backspace [
 214   local-scope
 215   assume-screen 10/width, 5/height
 216   # just one character in final line
 217   s:text <- new [ab
 218 cd]
 219   e:&:editor <- new-editor s, 0/left, 10/right
 220   assume-console [
 221     left-click 2, 0  # cursor at only character in final line
 222     press backspace
 223   ]
 224   run [
 225     editor-event-loop screen, console, e
 226     4:num/raw <- get *e, cursor-row:offset
 227     5:num/raw <- get *e, cursor-column:offset
 228   ]
 229   screen-should-contain [
 230     .          .
 231     .abcd      .
 232     .╌╌╌╌╌╌╌╌╌╌.
 233     .          .
 234   ]
 235   memory-should-contain [
 236     4 <- 1
 237     5 <- 2
 238   ]
 239 ]
 240 
 241 scenario editor-joins-and-wraps-lines-on-backspace [
 242   local-scope
 243   assume-screen 10/width, 5/height
 244   # initialize editor with two long-ish but non-wrapping lines
 245   s:text <- new [abc def
 246 ghi jkl]
 247   e:&:editor <- new-editor s, 0/left, 10/right
 248   editor-render screen, e
 249   $clear-trace
 250   # position the cursor at the start of the second and hit backspace
 251   assume-console [
 252     left-click 2, 0
 253     press backspace
 254   ]
 255   run [
 256     editor-event-loop screen, console, e
 257   ]
 258   # resulting single line should wrap correctly
 259   screen-should-contain [
 260     .          .
 261     .abc defgh↩.
 262     .i jkl     .
 263     .╌╌╌╌╌╌╌╌╌╌.
 264     .          .
 265   ]
 266 ]
 267 
 268 scenario editor-wraps-long-lines-on-backspace [
 269   local-scope
 270   assume-screen 10/width, 5/height
 271   # initialize editor in part of the screen with a long line
 272   e:&:editor <- new-editor [abc def ghij], 0/left, 8/right
 273   editor-render screen, e
 274   # confirm that it wraps
 275   screen-should-contain [
 276     .          .
 277     .abc def↩  .
 278     . ghij     .
 279     .╌╌╌╌╌╌╌╌  .
 280   ]
 281   $clear-trace
 282   # position the cursor somewhere in the middle of the top screen line and hit backspace
 283   assume-console [
 284     left-click 1, 4
 285     press backspace
 286   ]
 287   run [
 288     editor-event-loop screen, console, e
 289   ]
 290   # resulting single line should wrap correctly and not overflow its bounds
 291   screen-should-contain [
 292     .          .
 293     .abcdef ↩  .
 294     .ghij      .
 295     .╌╌╌╌╌╌╌╌  .
 296     .          .
 297   ]
 298 ]
 299 
 300 # delete - delete character at cursor
 301 
 302 scenario editor-handles-delete-key [
 303   local-scope
 304   assume-screen 10/width, 5/height
 305   e:&:editor <- new-editor [abc], 0/left, 10/right
 306   editor-render screen, e
 307   $clear-trace
 308   assume-console [
 309     press delete
 310   ]
 311   run [
 312     editor-event-loop screen, console, e
 313   ]
 314   screen-should-contain [
 315     .          .
 316     .bc        .
 317     .╌╌╌╌╌╌╌╌╌╌.
 318     .          .
 319   ]
 320   check-trace-count-for-label 3, [print-character]  # length of original line to overwrite
 321   $clear-trace
 322   assume-console [
 323     press delete
 324   ]
 325   run [
 326     editor-event-loop screen, console, e
 327   ]
 328   screen-should-contain [
 329     .          .
 330     .c         .
 331     .╌╌╌╌╌╌╌╌╌╌.
 332     .          .
 333   ]
 334   check-trace-count-for-label 2, [print-character]  # new length to overwrite
 335 ]
 336 
 337 after <handle-special-key> [
 338   {
 339     delete-next-character?:bool <- equal k, 65522/delete
 340     break-unless delete-next-character?
 341     <delete-character-begin>
 342     go-render?:bool, deleted-cell:&:duplex-list:char <- delete-at-cursor editor, screen
 343     <delete-character-end>
 344     return
 345   }
 346 ]
 347 
 348 def delete-at-cursor editor:&:editor, screen:&:screen -> go-render?:bool, deleted-cell:&:duplex-list:char, editor:&:editor, screen:&:screen [
 349   local-scope
 350   load-ingredients
 351   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 352   data:&:duplex-list:char <- get *editor, data:offset
 353   deleted-cell:&:duplex-list:char <- next before-cursor
 354   return-unless deleted-cell, 0/don't-render
 355   currc:char <- get *deleted-cell, value:offset
 356   data <- remove deleted-cell, data
 357   deleted-newline?:bool <- equal currc, 10/newline
 358   return-if deleted-newline?, 1/go-render
 359   # wasn't a newline? render rest of line
 360   curr:&:duplex-list:char <- next before-cursor  # refresh after remove above
 361   cursor-row:num <- get *editor, cursor-row:offset
 362   cursor-column:num <- get *editor, cursor-column:offset
 363   screen <- move-cursor screen, cursor-row, cursor-column
 364   curr-column:num <- copy cursor-column
 365   screen-width:num <- screen-width screen
 366   {
 367     # hit right margin? give up and let caller render
 368     at-right?:bool <- greater-or-equal curr-column, screen-width
 369     return-if at-right?, 1/go-render
 370     break-unless curr
 371     # newline? done.
 372     currc:char <- get *curr, value:offset
 373     at-newline?:bool <- equal currc, 10/newline
 374     break-if at-newline?
 375     screen <- print screen, currc
 376     curr-column <- add curr-column, 1
 377     curr <- next curr
 378     loop
 379   }
 380   # we're guaranteed not to be at the right margin
 381   space:char <- copy 32/space
 382   screen <- print screen, space
 383   go-render? <- copy 0/false
 384 ]
 385 
 386 # right arrow
 387 
 388 scenario editor-moves-cursor-right-with-key [
 389   local-scope
 390   assume-screen 10/width, 5/height
 391   e:&:editor <- new-editor [abc], 0/left, 10/right
 392   editor-render screen, e
 393   $clear-trace
 394   assume-console [
 395     press right-arrow
 396     type [0]
 397   ]
 398   run [
 399     editor-event-loop screen, console, e
 400   ]
 401   screen-should-contain [
 402     .          .
 403     .a0bc      .
 404     .╌╌╌╌╌╌╌╌╌╌.
 405     .          .
 406   ]
 407   check-trace-count-for-label 3, [print-character]  # 0 and following characters
 408 ]
 409 
 410 after <handle-special-key> [
 411   {
 412     move-to-next-character?:bool <- equal k, 65514/right-arrow
 413     break-unless move-to-next-character?
 414     # if not at end of text
 415     next-cursor:&:duplex-list:char <- next before-cursor
 416     break-unless next-cursor
 417     # scan to next character
 418     <move-cursor-begin>
 419     before-cursor <- copy next-cursor
 420     *editor <- put *editor, before-cursor:offset, before-cursor
 421     go-render?:bool <- move-cursor-coordinates-right editor, screen-height
 422     screen <- move-cursor screen, cursor-row, cursor-column
 423     undo-coalesce-tag:num <- copy 2/right-arrow
 424     <move-cursor-end>
 425     return
 426   }
 427 ]
 428 
 429 def move-cursor-coordinates-right editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
 430   local-scope
 431   load-ingredients
 432   before-cursor:&:duplex-list:char <- get *editor before-cursor:offset
 433   cursor-row:num <- get *editor, cursor-row:offset
 434   cursor-column:num <- get *editor, cursor-column:offset
 435   left:num <- get *editor, left:offset
 436   right:num <- get *editor, right:offset
 437   # if crossed a newline, move cursor to start of next row
 438   {
 439     old-cursor-character:char <- get *before-cursor, value:offset
 440     was-at-newline?:bool <- equal old-cursor-character, 10/newline
 441     break-unless was-at-newline?
 442     cursor-row <- add cursor-row, 1
 443     *editor <- put *editor, cursor-row:offset, cursor-row
 444     cursor-column <- copy left
 445     *editor <- put *editor, cursor-column:offset, cursor-column
 446     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
 447     return-unless below-screen?, 0/don't-render
 448     <scroll-down>
 449     cursor-row <- subtract cursor-row, 1  # bring back into screen range
 450     *editor <- put *editor, cursor-row:offset, cursor-row
 451     return 1/go-render
 452   }
 453   # if the line wraps, move cursor to start of next row
 454   {
 455     # if we're at the column just before the wrap indicator
 456     wrap-column:num <- subtract right, 1
 457     at-wrap?:bool <- equal cursor-column, wrap-column
 458     break-unless at-wrap?
 459     # and if next character isn't newline
 460     next:&:duplex-list:char <- next before-cursor
 461     break-unless next
 462     next-character:char <- get *next, value:offset
 463     newline?:bool <- equal next-character, 10/newline
 464     break-if newline?
 465     cursor-row <- add cursor-row, 1
 466     *editor <- put *editor, cursor-row:offset, cursor-row
 467     cursor-column <- copy left
 468     *editor <- put *editor, cursor-column:offset, cursor-column
 469     below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal
 470     return-unless below-screen?, 0/no-more-render
 471     <scroll-down>
 472     cursor-row <- subtract cursor-row, 1  # bring back into screen range
 473     *editor <- put *editor, cursor-row:offset, cursor-row
 474     return 1/go-render
 475   }
 476   # otherwise move cursor one character right
 477   cursor-column <- add cursor-column, 1
 478   *editor <- put *editor, cursor-column:offset, cursor-column
 479   go-render? <- copy 0/false
 480 ]
 481 
 482 scenario editor-moves-cursor-to-next-line-with-right-arrow [
 483   local-scope
 484   assume-screen 10/width, 5/height
 485   s:text <- new [abc
 486 d]
 487   e:&:editor <- new-editor s, 0/left, 10/right
 488   editor-render screen, e
 489   $clear-trace
 490   # type right-arrow a few times to get to start of second line
 491   assume-console [
 492     press right-arrow
 493     press right-arrow
 494     press right-arrow
 495     press right-arrow  # next line
 496   ]
 497   run [
 498     editor-event-loop screen, console, e
 499   ]
 500   check-trace-count-for-label 0, [print-character]
 501   # type something and ensure it goes where it should
 502   assume-console [
 503     type [0]
 504   ]
 505   run [
 506     editor-event-loop screen, console, e
 507   ]
 508   screen-should-contain [
 509     .          .
 510     .abc       .
 511     .0d        .
 512     .╌╌╌╌╌╌╌╌╌╌.
 513     .          .
 514   ]
 515   check-trace-count-for-label 2, [print-character]  # new length of second line
 516 ]
 517 
 518 scenario editor-moves-cursor-to-next-line-with-right-arrow-2 [
 519   local-scope
 520   assume-screen 10/width, 5/height
 521   s:text <- new [abc
 522 d]
 523   e:&:editor <- new-editor s, 1/left, 10/right
 524   editor-render screen, e
 525   assume-console [
 526     press right-arrow
 527     press right-arrow
 528     press right-arrow
 529     press right-arrow  # next line
 530     type [0]
 531   ]
 532   run [
 533     editor-event-loop screen, console, e
 534   ]
 535   screen-should-contain [
 536     .          .
 537     . abc      .
 538     . 0d       .
 539     . ╌╌╌╌╌╌╌╌╌.
 540     .          .
 541   ]
 542 ]
 543 
 544 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow [
 545   local-scope
 546   assume-screen 10/width, 5/height
 547   e:&:editor <- new-editor [abcdef], 0/left, 5/right
 548   editor-render screen, e
 549   $clear-trace
 550   assume-console [
 551     left-click 1, 3
 552     press right-arrow
 553   ]
 554   run [
 555     editor-event-loop screen, console, e
 556     3:num/raw <- get *e, cursor-row:offset
 557     4:num/raw <- get *e, cursor-column:offset
 558   ]
 559   screen-should-contain [
 560     .          .
 561     .abcd↩     .
 562     .ef        .
 563     .╌╌╌╌╌     .
 564     .          .
 565   ]
 566   memory-should-contain [
 567     3 <- 2
 568     4 <- 0
 569   ]
 570   check-trace-count-for-label 0, [print-character]
 571 ]
 572 
 573 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-2 [
 574   local-scope
 575   assume-screen 10/width, 5/height
 576   # line just barely wrapping
 577   e:&:editor <- new-editor [abcde], 0/left, 5/right
 578   editor-render screen, e
 579   $clear-trace
 580   # position cursor at last character before wrap and hit right-arrow
 581   assume-console [
 582     left-click 1, 3
 583     press right-arrow
 584   ]
 585   run [
 586     editor-event-loop screen, console, e
 587     3:num/raw <- get *e, cursor-row:offset
 588     4:num/raw <- get *e, cursor-column:offset
 589   ]
 590   memory-should-contain [
 591     3 <- 2
 592     4 <- 0
 593   ]
 594   # now hit right arrow again
 595   assume-console [
 596     press right-arrow
 597   ]
 598   run [
 599     editor-event-loop screen, console, e
 600     3:num/raw <- get *e, cursor-row:offset
 601     4:num/raw <- get *e, cursor-column:offset
 602   ]
 603   memory-should-contain [
 604     3 <- 2
 605     4 <- 1
 606   ]
 607   check-trace-count-for-label 0, [print-character]
 608 ]
 609 
 610 scenario editor-moves-cursor-to-next-wrapped-line-with-right-arrow-3 [
 611   local-scope
 612   assume-screen 10/width, 5/height
 613   e:&:editor <- new-editor [abcdef], 1/left, 6/right
 614   editor-render screen, e
 615   $clear-trace
 616   assume-console [
 617     left-click 1, 4
 618     press right-arrow
 619   ]
 620   run [
 621     editor-event-loop screen, console, e
 622     3:num/raw <- get *e, cursor-row:offset
 623     4:num/raw <- get *e, cursor-column:offset
 624   ]
 625   screen-should-contain [
 626     .          .
 627     . abcd↩    .
 628     . ef       .
 629     . ╌╌╌╌╌    .
 630     .          .
 631   ]
 632   memory-should-contain [
 633     3 <- 2
 634     4 <- 1
 635   ]
 636   check-trace-count-for-label 0, [print-character]
 637 ]
 638 
 639 scenario editor-moves-cursor-to-next-line-with-right-arrow-at-end-of-line [
 640   local-scope
 641   assume-screen 10/width, 5/height
 642   s:text <- new [abc
 643 d]
 644   e:&:editor <- new-editor s, 0/left, 10/right
 645   editor-render screen, e
 646   $clear-trace
 647   # move to end of line, press right-arrow, type a character
 648   assume-console [
 649     left-click 1, 3
 650     press right-arrow
 651     type [0]
 652   ]
 653   run [
 654     editor-event-loop screen, console, e
 655   ]
 656   # new character should be in next line
 657   screen-should-contain [
 658     .          .
 659     .abc       .
 660     .0d        .
 661     .╌╌╌╌╌╌╌╌╌╌.
 662     .          .
 663   ]
 664   check-trace-count-for-label 2, [print-character]
 665 ]
 666 
 667 # todo: ctrl-right: next word-end
 668 
 669 # left arrow
 670 
 671 scenario editor-moves-cursor-left-with-key [
 672   local-scope
 673   assume-screen 10/width, 5/height
 674   e:&:editor <- new-editor [abc], 0/left, 10/right
 675   editor-render screen, e
 676   $clear-trace
 677   assume-console [
 678     left-click 1, 2
 679     press left-arrow
 680     type [0]
 681   ]
 682   run [
 683     editor-event-loop screen, console, e
 684   ]
 685   screen-should-contain [
 686     .          .
 687     .a0bc      .
 688     .╌╌╌╌╌╌╌╌╌╌.
 689     .          .
 690   ]
 691   check-trace-count-for-label 3, [print-character]
 692 ]
 693 
 694 after <handle-special-key> [
 695   {
 696     move-to-previous-character?:bool <- equal k, 65515/left-arrow
 697     break-unless move-to-previous-character?
 698     trace 10, [app], [left arrow]
 699     # if not at start of text (before-cursor at § sentinel)
 700     prev:&:duplex-list:char <- prev before-cursor
 701     return-unless prev, 0/don't-render
 702     <move-cursor-begin>
 703     go-render? <- move-cursor-coordinates-left editor
 704     before-cursor <- copy prev
 705     *editor <- put *editor, before-cursor:offset, before-cursor
 706     undo-coalesce-tag:num <- copy 1/left-arrow
 707     <move-cursor-end>
 708     return
 709   }
 710 ]
 711 
 712 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line [
 713   local-scope
 714   assume-screen 10/width, 5/height
 715   # initialize editor with two lines
 716   s:text <- new [abc
 717 d]
 718   e:&:editor <- new-editor s, 0/left, 10/right
 719   editor-render screen, e
 720   $clear-trace
 721   # position cursor at start of second line (so there's no previous newline)
 722   assume-console [
 723     left-click 2, 0
 724     press left-arrow
 725   ]
 726   run [
 727     editor-event-loop screen, console, e
 728     3:num/raw <- get *e, cursor-row:offset
 729     4:num/raw <- get *e, cursor-column:offset
 730   ]
 731   memory-should-contain [
 732     3 <- 1
 733     4 <- 3
 734   ]
 735   check-trace-count-for-label 0, [print-character]
 736 ]
 737 
 738 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-2 [
 739   local-scope
 740   assume-screen 10/width, 5/height
 741   # initialize editor with three lines
 742   s:text <- new [abc
 743 def
 744 g]
 745   e:&:editor <- new-editor s:text, 0/left, 10/right
 746   editor-render screen, e
 747   $clear-trace
 748   # position cursor further down (so there's a newline before the character at
 749   # the cursor)
 750   assume-console [
 751     left-click 3, 0
 752     press left-arrow
 753     type [0]
 754   ]
 755   run [
 756     editor-event-loop screen, console, e
 757   ]
 758   screen-should-contain [
 759     .          .
 760     .abc       .
 761     .def0      .
 762     .g         .
 763     .╌╌╌╌╌╌╌╌╌╌.
 764   ]
 765   check-trace-count-for-label 1, [print-character]  # just the '0'
 766 ]
 767 
 768 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-3 [
 769   local-scope
 770   assume-screen 10/width, 5/height
 771   s:text <- new [abc
 772 def
 773 g]
 774   e:&:editor <- new-editor s, 0/left, 10/right
 775   editor-render screen, e
 776   $clear-trace
 777   # position cursor at start of text, press left-arrow, then type a character
 778   assume-console [
 779     left-click 1, 0
 780     press left-arrow
 781     type [0]
 782   ]
 783   run [
 784     editor-event-loop screen, console, e
 785   ]
 786   # left-arrow should have had no effect
 787   screen-should-contain [
 788     .          .
 789     .0abc      .
 790     .def       .
 791     .g         .
 792     .╌╌╌╌╌╌╌╌╌╌.
 793   ]
 794   check-trace-count-for-label 4, [print-character]  # length of first line
 795 ]
 796 
 797 scenario editor-moves-cursor-to-previous-line-with-left-arrow-at-start-of-line-4 [
 798   local-scope
 799   assume-screen 10/width, 5/height
 800   # initialize editor with text containing an empty line
 801   s:text <- new [abc
 802 
 803 d]
 804   e:&:editor <- new-editor s, 0/left, 10/right
 805   editor-render screen, e:&:editor
 806   $clear-trace
 807   # position cursor right after empty line
 808   assume-console [
 809     left-click 3, 0
 810     press left-arrow
 811     type [0]
 812   ]
 813   run [
 814     editor-event-loop screen, console, e
 815   ]
 816   screen-should-contain [
 817     .          .
 818     .abc       .
 819     .0         .
 820     .d         .
 821     .╌╌╌╌╌╌╌╌╌╌.
 822   ]
 823   check-trace-count-for-label 1, [print-character]  # just the '0'
 824 ]
 825 
 826 scenario editor-moves-across-screen-lines-across-wrap-with-left-arrow [
 827   local-scope
 828   assume-screen 10/width, 5/height
 829   # initialize editor with a wrapping line
 830   e:&:editor <- new-editor [abcdef], 0/left, 5/right
 831   editor-render screen, e
 832   $clear-trace
 833   screen-should-contain [
 834     .          .
 835     .abcd↩     .
 836     .ef        .
 837     .╌╌╌╌╌     .
 838     .          .
 839   ]
 840   # position cursor right after empty line
 841   assume-console [
 842     left-click 2, 0
 843     press left-arrow
 844   ]
 845   run [
 846     editor-event-loop screen, console, e
 847     3:num/raw <- get *e, cursor-row:offset
 848     4:num/raw <- get *e, cursor-column:offset
 849   ]
 850   memory-should-contain [
 851     3 <- 1  # previous row
 852     4 <- 3  # right margin except wrap icon
 853   ]
 854   check-trace-count-for-label 0, [print-character]
 855 ]
 856 
 857 scenario editor-moves-across-screen-lines-to-wrapping-line-with-left-arrow [
 858   local-scope
 859   assume-screen 10/width, 5/height
 860   # initialize editor with a wrapping line followed by a second line
 861   s:text <- new [abcdef
 862 g]
 863   e:&:editor <- new-editor s, 0/left, 5/right
 864   editor-render screen, e
 865   $clear-trace
 866   screen-should-contain [
 867     .          .
 868     .abcd↩     .
 869     .ef        .
 870     .g         .
 871     .╌╌╌╌╌     .
 872   ]
 873   # position cursor right after empty line
 874   assume-console [
 875     left-click 3, 0
 876     press left-arrow
 877   ]
 878   run [
 879     editor-event-loop screen, console, e
 880     3:num/raw <- get *e, cursor-row:offset
 881     4:num/raw <- get *e, cursor-column:offset
 882   ]
 883   memory-should-contain [
 884     3 <- 2  # previous row
 885     4 <- 2  # end of wrapped line
 886   ]
 887   check-trace-count-for-label 0, [print-character]
 888 ]
 889 
 890 scenario editor-moves-across-screen-lines-to-non-wrapping-line-with-left-arrow [
 891   local-scope
 892   assume-screen 10/width, 5/height
 893   # initialize editor with a line on the verge of wrapping, followed by a second line
 894   s:text <- new [abcd
 895 e]
 896   e:&:editor <- new-editor s, 0/left, 5/right
 897   editor-render screen, e
 898   $clear-trace
 899   screen-should-contain [
 900     .          .
 901     .abcd      .
 902     .e         .
 903     .╌╌╌╌╌     .
 904     .          .
 905   ]
 906   # position cursor right after empty line
 907   assume-console [
 908     left-click 2, 0
 909     press left-arrow
 910   ]
 911   run [
 912     editor-event-loop screen, console, e
 913     3:num/raw <- get *e, cursor-row:offset
 914     4:num/raw <- get *e, cursor-column:offset
 915   ]
 916   memory-should-contain [
 917     3 <- 1  # previous row
 918     4 <- 4  # end of wrapped line
 919   ]
 920   check-trace-count-for-label 0, [print-character]
 921 ]
 922 
 923 # todo: ctrl-left: previous word-start
 924 
 925 # up arrow
 926 
 927 scenario editor-moves-to-previous-line-with-up-arrow [
 928   local-scope
 929   assume-screen 10/width, 5/height
 930   s:text <- new [abc
 931 def]
 932   e:&:editor <- new-editor s, 0/left, 10/right
 933   editor-render screen, e
 934   $clear-trace
 935   assume-console [
 936     left-click 2, 1
 937     press up-arrow
 938   ]
 939   run [
 940     editor-event-loop screen, console, e
 941     3:num/raw <- get *e, cursor-row:offset
 942     4:num/raw <- get *e, cursor-column:offset
 943   ]
 944   memory-should-contain [
 945     3 <- 1
 946     4 <- 1
 947   ]
 948   check-trace-count-for-label 0, [print-character]
 949   assume-console [
 950     type [0]
 951   ]
 952   run [
 953     editor-event-loop screen, console, e
 954   ]
 955   screen-should-contain [
 956     .          .
 957     .a0bc      .
 958     .def       .
 959     .╌╌╌╌╌╌╌╌╌╌.
 960     .          .
 961   ]
 962 ]
 963 
 964 after <handle-special-key> [
 965   {
 966     move-to-previous-line?:bool <- equal k, 65517/up-arrow
 967     break-unless move-to-previous-line?
 968     <move-cursor-begin>
 969     go-render? <- move-to-previous-line editor
 970     undo-coalesce-tag:num <- copy 3/up-arrow
 971     <move-cursor-end>
 972     return
 973   }
 974 ]
 975 
 976 def move-to-previous-line editor:&:editor -> go-render?:bool, editor:&:editor [
 977   local-scope
 978   load-ingredients
 979   go-render?:bool <- copy 0/false
 980   cursor-row:num <- get *editor, cursor-row:offset
 981   cursor-column:num <- get *editor, cursor-column:offset
 982   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 983   left:num <- get *editor, left:offset
 984   right:num <- get *editor, right:offset
 985   already-at-top?:bool <- lesser-or-equal cursor-row, 1/top
 986   {
 987     # if cursor not at top, move it
 988     break-if already-at-top?
 989     # if not at newline, move to start of line (previous newline)
 990     # then scan back another line
 991     # if either step fails, give up without modifying cursor or coordinates
 992     curr:&:duplex-list:char <- copy before-cursor
 993     {
 994       old:&:duplex-list:char <- copy curr
 995       c2:char <- get *curr, value:offset
 996       at-newline?:bool <- equal c2, 10/newline
 997       break-if at-newline?
 998       curr:&:duplex-list:char <- before-previous-line curr, editor
 999       no-motion?:bool <- equal curr, old
1000       return-if no-motion?
1001     }
1002     {
1003       old <- copy curr
1004       curr <- before-previous-line curr, editor
1005       no-motion?:bool <- equal curr, old
1006       return-if no-motion?
1007     }
1008     before-cursor <- copy curr
1009     *editor <- put *editor, before-cursor:offset, before-cursor
1010     cursor-row <- subtract cursor-row, 1
1011     *editor <- put *editor, cursor-row:offset, cursor-row
1012     # scan ahead to right column or until end of line
1013     target-column:num <- copy cursor-column
1014     cursor-column <- copy left
1015     *editor <- put *editor, cursor-column:offset, cursor-column
1016     {
1017       done?:bool <- greater-or-equal cursor-column, target-column
1018       break-if done?
1019       curr:&:duplex-list:char <- next before-cursor
1020       break-unless curr
1021       currc:char <- get *curr, value:offset
1022       at-newline?:bool <- equal currc, 10/newline
1023       break-if at-newline?
1024       #
1025       before-cursor <- copy curr
1026       *editor <- put *editor, before-cursor:offset, before-cursor
1027       cursor-column <- add cursor-column, 1
1028       *editor <- put *editor, cursor-column:offset, cursor-column
1029       loop
1030     }
1031     return
1032   }
1033   {
1034     # if cursor already at top, scroll up
1035     break-unless already-at-top?
1036     <scroll-up>
1037     return 1/go-render
1038   }
1039 ]
1040 
1041 scenario editor-adjusts-column-at-previous-line [
1042   local-scope
1043   assume-screen 10/width, 5/height
1044   s:text <- new [ab
1045 def]
1046   e:&:editor <- new-editor s, 0/left, 10/right
1047   editor-render screen, e
1048   $clear-trace
1049   assume-console [
1050     left-click 2, 3
1051     press up-arrow
1052   ]
1053   run [
1054     editor-event-loop screen, console, e
1055     3:num/raw <- get *e, cursor-row:offset
1056     4:num/raw <- get *e, cursor-column:offset
1057   ]
1058   memory-should-contain [
1059     3 <- 1
1060     4 <- 2
1061   ]
1062   check-trace-count-for-label 0, [print-character]
1063   assume-console [
1064     type [0]
1065   ]
1066   run [
1067     editor-event-loop screen, console, e
1068   ]
1069   screen-should-contain [
1070     .          .
1071     .ab0       .
1072     .def       .
1073     .╌╌╌╌╌╌╌╌╌╌.
1074     .          .
1075   ]
1076 ]
1077 
1078 scenario editor-adjusts-column-at-empty-line [
1079   local-scope
1080   assume-screen 10/width, 5/height
1081   s:text <- new [
1082 def]
1083   e:&:editor <- new-editor s, 0/left, 10/right
1084   editor-render screen, e
1085   $clear-trace
1086   assume-console [
1087     left-click 2, 3
1088     press up-arrow
1089   ]
1090   run [
1091     editor-event-loop screen, console, e
1092     3:num/raw <- get *e, cursor-row:offset
1093     4:num/raw <- get *e, cursor-column:offset
1094   ]
1095   memory-should-contain [
1096     3 <- 1
1097     4 <- 0
1098   ]
1099   check-trace-count-for-label 0, [print-character]
1100   assume-console [
1101     type [0]
1102   ]
1103   run [
1104     editor-event-loop screen, console, e
1105   ]
1106   screen-should-contain [
1107     .          .
1108     .0         .
1109     .def       .
1110     .╌╌╌╌╌╌╌╌╌╌.
1111     .          .
1112   ]
1113 ]
1114 
1115 scenario editor-moves-to-previous-line-from-left-margin [
1116   local-scope
1117   assume-screen 10/width, 5/height
1118   # start out with three lines
1119   s:text <- new [abc
1120 def
1121 ghi]
1122   e:&:editor <- new-editor s, 0/left, 10/right
1123   editor-render screen, e
1124   $clear-trace
1125   # click on the third line and hit up-arrow, so you end up just after a newline
1126   assume-console [
1127     left-click 3, 0
1128     press up-arrow
1129   ]
1130   run [
1131     editor-event-loop screen, console, e
1132     3:num/raw <- get *e, cursor-row:offset
1133     4:num/raw <- get *e, cursor-column:offset
1134   ]
1135   memory-should-contain [
1136     3 <- 2
1137     4 <- 0
1138   ]
1139   check-trace-count-for-label 0, [print-character]
1140   assume-console [
1141     type [0]
1142   ]
1143   run [
1144     editor-event-loop screen, console, e
1145   ]
1146   screen-should-contain [
1147     .          .
1148     .abc       .
1149     .0def      .
1150     .ghi       .
1151     .╌╌╌╌╌╌╌╌╌╌.
1152   ]
1153 ]
1154 
1155 # down arrow
1156 
1157 scenario editor-moves-to-next-line-with-down-arrow [
1158   local-scope
1159   assume-screen 10/width, 5/height
1160   s:text <- new [abc
1161 def]
1162   e:&:editor <- new-editor s, 0/left, 10/right
1163   editor-render screen, e
1164   $clear-trace
1165   # cursor starts out at (1, 0)
1166   assume-console [
1167     press down-arrow
1168   ]
1169   run [
1170     editor-event-loop screen, console, e
1171     3:num/raw <- get *e, cursor-row:offset
1172     4:num/raw <- get *e, cursor-column:offset
1173   ]
1174   # ..and ends at (2, 0)
1175   memory-should-contain [
1176     3 <- 2
1177     4 <- 0
1178   ]
1179   check-trace-count-for-label 0, [print-character]
1180   assume-console [
1181     type [0]
1182   ]
1183   run [
1184     editor-event-loop screen, console, e
1185   ]
1186   screen-should-contain [
1187     .          .
1188     .abc       .
1189     .0def      .
1190     .╌╌╌╌╌╌╌╌╌╌.
1191     .          .
1192   ]
1193 ]
1194 
1195 after <handle-special-key> [
1196   {
1197     move-to-next-line?:bool <- equal k, 65516/down-arrow
1198     break-unless move-to-next-line?
1199     <move-cursor-begin>
1200     go-render? <- move-to-next-line editor, screen-height
1201     undo-coalesce-tag:num <- copy 4/down-arrow
1202     <move-cursor-end>
1203     return
1204   }
1205 ]
1206 
1207 def move-to-next-line editor:&:editor, screen-height:num -> go-render?:bool, editor:&:editor [
1208   local-scope
1209   load-ingredients
1210   cursor-row:num <- get *editor, cursor-row:offset
1211   cursor-column:num <- get *editor, cursor-column:offset
1212   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1213   left:num <- get *editor, left:offset
1214   right:num <- get *editor, right:offset
1215   last-line:num <- subtract screen-height, 1
1216   already-at-bottom?:bool <- greater-or-equal cursor-row, last-line
1217   {
1218     # if cursor not at bottom, move it
1219     break-if already-at-bottom?
1220     # scan to start of next line, then to right column or until end of line
1221     max:num <- subtract right, left
1222     next-line:&:duplex-list:char <- before-start-of-next-line before-cursor, max
1223     {
1224       # already at end of buffer? try to scroll up (so we can see more
1225       # warnings or sandboxes below)
1226       no-motion?:bool <- equal next-line, before-cursor
1227       break-unless no-motion?
1228       scroll?:bool <- greater-than cursor-row, 1
1229       break-if scroll?, +try-to-scroll
1230       return 0/don't-render
1231     }
1232     cursor-row <- add cursor-row, 1
1233     *editor <- put *editor, cursor-row:offset, cursor-row
1234     before-cursor <- copy next-line
1235     *editor <- put *editor, before-cursor:offset, before-cursor
1236     target-column:num <- copy cursor-column
1237     cursor-column <- copy left
1238     *editor <- put *editor, cursor-column:offset, cursor-column
1239     {
1240       done?:bool <- greater-or-equal cursor-column, target-column
1241       break-if done?
1242       curr:&:duplex-list:char <- next before-cursor
1243       break-unless curr
1244       currc:char <- get *curr, value:offset
1245       at-newline?:bool <- equal currc, 10/newline
1246       break-if at-newline?
1247       #
1248       before-cursor <- copy curr
1249       *editor <- put *editor, before-cursor:offset, before-cursor
1250       cursor-column <- add cursor-column, 1
1251       *editor <- put *editor, cursor-column:offset, cursor-column
1252       loop
1253     }
1254     return 0/don't-render
1255   }
1256   +try-to-scroll
1257   <scroll-down>
1258   go-render? <- copy 1/true
1259 ]
1260 
1261 scenario editor-adjusts-column-at-next-line [
1262   local-scope
1263   assume-screen 10/width, 5/height
1264   s:text <- new [abc
1265 de]
1266   e:&:editor <- new-editor s, 0/left, 10/right
1267   editor-render screen, e
1268   $clear-trace
1269   assume-console [
1270     left-click 1, 3
1271     press down-arrow
1272   ]
1273   run [
1274     editor-event-loop screen, console, e
1275     3:num/raw <- get *e, cursor-row:offset
1276     4:num/raw <- get *e, cursor-column:offset
1277   ]
1278   memory-should-contain [
1279     3 <- 2
1280     4 <- 2
1281   ]
1282   check-trace-count-for-label 0, [print-character]
1283   assume-console [
1284     type [0]
1285   ]
1286   run [
1287     editor-event-loop screen, console, e
1288   ]
1289   screen-should-contain [
1290     .          .
1291     .abc       .
1292     .de0       .
1293     .╌╌╌╌╌╌╌╌╌╌.
1294     .          .
1295   ]
1296 ]
1297 
1298 # ctrl-a/home - move cursor to start of line
1299 
1300 scenario editor-moves-to-start-of-line-with-ctrl-a [
1301   local-scope
1302   assume-screen 10/width, 5/height
1303   s:text <- new [123
1304 456]
1305   e:&:editor <- new-editor s, 0/left, 10/right
1306   editor-render screen, e
1307   $clear-trace
1308   # start on second line, press ctrl-a
1309   assume-console [
1310     left-click 2, 3
1311     press ctrl-a
1312   ]
1313   run [
1314     editor-event-loop screen, console, e
1315     4:num/raw <- get *e, cursor-row:offset
1316     5:num/raw <- get *e, cursor-column:offset
1317   ]
1318   # cursor moves to start of line
1319   memory-should-contain [
1320     4 <- 2
1321     5 <- 0
1322   ]
1323   check-trace-count-for-label 0, [print-character]
1324 ]
1325 
1326 after <handle-special-character> [
1327   {
1328     move-to-start-of-line?:bool <- equal c, 1/ctrl-a
1329     break-unless move-to-start-of-line?
1330     <move-cursor-begin>
1331     move-to-start-of-line editor
1332     undo-coalesce-tag:num <- copy 0/never
1333     <move-cursor-end>
1334     return 0/don't-render
1335   }
1336 ]
1337 
1338 after <handle-special-key> [
1339   {
1340     move-to-start-of-line?:bool <- equal k, 65521/home
1341     break-unless move-to-start-of-line?
1342     <move-cursor-begin>
1343     move-to-start-of-line editor
1344     undo-coalesce-tag:num <- copy 0/never
1345     <move-cursor-end>
1346     return 0/don't-render
1347   }
1348 ]
1349 
1350 def move-to-start-of-line editor:&:editor -> editor:&:editor [
1351   local-scope
1352   load-ingredients
1353   # update cursor column
1354   left:num <- get *editor, left:offset
1355   cursor-column:num <- copy left
1356   *editor <- put *editor, cursor-column:offset, cursor-column
1357   # update before-cursor
1358   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1359   init:&:duplex-list:char <- get *editor, data:offset
1360   # while not at start of line, move
1361   {
1362     at-start-of-text?:bool <- equal before-cursor, init
1363     break-if at-start-of-text?
1364     prev:char <- get *before-cursor, value:offset
1365     at-start-of-line?:bool <- equal prev, 10/newline
1366     break-if at-start-of-line?
1367     before-cursor <- prev before-cursor
1368     *editor <- put *editor, before-cursor:offset, before-cursor
1369     assert before-cursor, [move-to-start-of-line tried to move before start of text]
1370     loop
1371   }
1372 ]
1373 
1374 scenario editor-moves-to-start-of-line-with-ctrl-a-2 [
1375   local-scope
1376   assume-screen 10/width, 5/height
1377   s:text <- new [123
1378 456]
1379   e:&:editor <- new-editor s, 0/left, 10/right
1380   editor-render screen, e
1381   $clear-trace
1382   # start on first line (no newline before), press ctrl-a
1383   assume-console [
1384     left-click 1, 3
1385     press ctrl-a
1386   ]
1387   run [
1388     editor-event-loop screen, console, e
1389     4:num/raw <- get *e, cursor-row:offset
1390     5:num/raw <- get *e, cursor-column:offset
1391   ]
1392   # cursor moves to start of line
1393   memory-should-contain [
1394     4 <- 1
1395     5 <- 0
1396   ]
1397   check-trace-count-for-label 0, [print-character]
1398 ]
1399 
1400 scenario editor-moves-to-start-of-line-with-home [
1401   local-scope
1402   assume-screen 10/width, 5/height
1403   s:text <- new [123
1404 456]
1405   e:&:editor <- new-editor s, 0/left, 10/right
1406   $clear-trace
1407   # start on second line, press 'home'
1408   assume-console [
1409     left-click 2, 3
1410     press home
1411   ]
1412   run [
1413     editor-event-loop screen, console, e
1414     3:num/raw <- get *e, cursor-row:offset
1415     4:num/raw <- get *e, cursor-column:offset
1416   ]
1417   # cursor moves to start of line
1418   memory-should-contain [
1419     3 <- 2
1420     4 <- 0
1421   ]
1422   check-trace-count-for-label 0, [print-character]
1423 ]
1424 
1425 scenario editor-moves-to-start-of-line-with-home-2 [
1426   local-scope
1427   assume-screen 10/width, 5/height
1428   s:text <- new [123
1429 456]
1430   e:&:editor <- new-editor s, 0/left, 10/right
1431   editor-render screen, e
1432   $clear-trace
1433   # start on first line (no newline before), press 'home'
1434   assume-console [
1435     left-click 1, 3
1436     press home
1437   ]
1438   run [
1439     editor-event-loop screen, console, e
1440     3:num/raw <- get *e, cursor-row:offset
1441     4:num/raw <- get *e, cursor-column:offset
1442   ]
1443   # cursor moves to start of line
1444   memory-should-contain [
1445     3 <- 1
1446     4 <- 0
1447   ]
1448   check-trace-count-for-label 0, [print-character]
1449 ]
1450 
1451 # ctrl-e/end - move cursor to end of line
1452 
1453 scenario editor-moves-to-end-of-line-with-ctrl-e [
1454   local-scope
1455   assume-screen 10/width, 5/height
1456   s:text <- new [123
1457 456]
1458   e:&:editor <- new-editor s, 0/left, 10/right
1459   editor-render screen, e
1460   $clear-trace
1461   # start on first line, press ctrl-e
1462   assume-console [
1463     left-click 1, 1
1464     press ctrl-e
1465   ]
1466   run [
1467     editor-event-loop screen, console, e
1468     4:num/raw <- get *e, cursor-row:offset
1469     5:num/raw <- get *e, cursor-column:offset
1470   ]
1471   # cursor moves to end of line
1472   memory-should-contain [
1473     4 <- 1
1474     5 <- 3
1475   ]
1476   check-trace-count-for-label 0, [print-character]
1477   # editor inserts future characters at cursor
1478   assume-console [
1479     type [z]
1480   ]
1481   run [
1482     editor-event-loop screen, console, e
1483     4:num/raw <- get *e, cursor-row:offset
1484     5:num/raw <- get *e, cursor-column:offset
1485   ]
1486   memory-should-contain [
1487     4 <- 1
1488     5 <- 4
1489   ]
1490   screen-should-contain [
1491     .          .
1492     .123z      .
1493     .456       .
1494     .╌╌╌╌╌╌╌╌╌╌.
1495     .          .
1496   ]
1497   check-trace-count-for-label 1, [print-character]
1498 ]
1499 
1500 after <handle-special-character> [
1501   {
1502     move-to-end-of-line?:bool <- equal c, 5/ctrl-e
1503     break-unless move-to-end-of-line?
1504     <move-cursor-begin>
1505     move-to-end-of-line editor
1506     undo-coalesce-tag:num <- copy 0/never
1507     <move-cursor-end>
1508     return 0/don't-render
1509   }
1510 ]
1511 
1512 after <handle-special-key> [
1513   {
1514     move-to-end-of-line?:bool <- equal k, 65520/end
1515     break-unless move-to-end-of-line?
1516     <move-cursor-begin>
1517     move-to-end-of-line editor
1518     undo-coalesce-tag:num <- copy 0/never
1519     <move-cursor-end>
1520     return 0/don't-render
1521   }
1522 ]
1523 
1524 def move-to-end-of-line editor:&:editor -> editor:&:editor [
1525   local-scope
1526   load-ingredients
1527   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1528   cursor-column:num <- get *editor, cursor-column:offset
1529   # while not at start of line, move
1530   {
1531     next:&:duplex-list:char <- next before-cursor
1532     break-unless next  # end of text
1533     nextc:char <- get *next, value:offset
1534     at-end-of-line?:bool <- equal nextc, 10/newline
1535     break-if at-end-of-line?
1536     before-cursor <- copy next
1537     *editor <- put *editor, before-cursor:offset, before-cursor
1538     cursor-column <- add cursor-column, 1
1539     *editor <- put *editor, cursor-column:offset, cursor-column
1540     loop
1541   }
1542 ]
1543 
1544 scenario editor-moves-to-end-of-line-with-ctrl-e-2 [
1545   local-scope
1546   assume-screen 10/width, 5/height
1547   s:text <- new [123
1548 456]
1549   e:&:editor <- new-editor s, 0/left, 10/right
1550   editor-render screen, e
1551   $clear-trace
1552   # start on second line (no newline after), press ctrl-e
1553   assume-console [
1554     left-click 2, 1
1555     press ctrl-e
1556   ]
1557   run [
1558     editor-event-loop screen, console, e
1559     4:num/raw <- get *e, cursor-row:offset
1560     5:num/raw <- get *e, cursor-column:offset
1561   ]
1562   # cursor moves to end of line
1563   memory-should-contain [
1564     4 <- 2
1565     5 <- 3
1566   ]
1567   check-trace-count-for-label 0, [print-character]
1568 ]
1569 
1570 scenario editor-moves-to-end-of-line-with-end [
1571   local-scope
1572   assume-screen 10/width, 5/height
1573   s:text <- new [123
1574 456]
1575   e:&:editor <- new-editor s, 0/left, 10/right
1576   editor-render screen, e
1577   $clear-trace
1578   # start on first line, press 'end'
1579   assume-console [
1580     left-click 1, 1
1581     press end
1582   ]
1583   run [
1584     editor-event-loop screen, console, e
1585     3:num/raw <- get *e, cursor-row:offset
1586     4:num/raw <- get *e, cursor-column:offset
1587   ]
1588   # cursor moves to end of line
1589   memory-should-contain [
1590     3 <- 1
1591     4 <- 3
1592   ]
1593   check-trace-count-for-label 0, [print-character]
1594 ]
1595 
1596 scenario editor-moves-to-end-of-line-with-end-2 [
1597   local-scope
1598   assume-screen 10/width, 5/height
1599   s:text <- new [123
1600 456]
1601   e:&:editor <- new-editor s, 0/left, 10/right
1602   editor-render screen, e
1603   $clear-trace
1604   # start on second line (no newline after), press 'end'
1605   assume-console [
1606     left-click 2, 1
1607     press end
1608   ]
1609   run [
1610     editor-event-loop screen, console, e
1611     3:num/raw <- get *e, cursor-row:offset
1612     4:num/raw <- get *e, cursor-column:offset
1613   ]
1614   # cursor moves to end of line
1615   memory-should-contain [
1616     3 <- 2
1617     4 <- 3
1618   ]
1619   check-trace-count-for-label 0, [print-character]
1620 ]
1621 
1622 # ctrl-u - delete text from start of line until (but not at) cursor
1623 
1624 scenario editor-deletes-to-start-of-line-with-ctrl-u [
1625   local-scope
1626   assume-screen 10/width, 5/height
1627   s:text <- new [123
1628 456]
1629   e:&:editor <- new-editor s, 0/left, 10/right
1630   # start on second line, press ctrl-u
1631   assume-console [
1632     left-click 2, 2
1633     press ctrl-u
1634   ]
1635   run [
1636     editor-event-loop screen, console, e
1637   ]
1638   # cursor deletes to start of line
1639   screen-should-contain [
1640     .          .
1641     .123       .
1642     .6         .
1643     .╌╌╌╌╌╌╌╌╌╌.
1644     .          .
1645   ]
1646 ]
1647 
1648 after <handle-special-character> [
1649   {
1650     delete-to-start-of-line?:bool <- equal c, 21/ctrl-u
1651     break-unless delete-to-start-of-line?
1652     <delete-to-start-of-line-begin>
1653     deleted-cells:&:duplex-list:char <- delete-to-start-of-line editor
1654     <delete-to-start-of-line-end>
1655     return 1/go-render
1656   }
1657 ]
1658 
1659 def delete-to-start-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
1660   local-scope
1661   load-ingredients
1662   # compute range to delete
1663   init:&:duplex-list:char <- get *editor, data:offset
1664   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
1665   start:&:duplex-list:char <- copy before-cursor
1666   end:&:duplex-list:char <- next before-cursor
1667   {
1668     at-start-of-text?:bool <- equal start, init
1669     break-if at-start-of-text?
1670     curr:char <- get *start, value:offset
1671     at-start-of-line?:bool <- equal curr, 10/newline
1672     break-if at-start-of-line?
1673     start <- prev start
1674     assert start, [delete-to-start-of-line tried to move before start of text]
1675     loop
1676   }
1677   # snip it out
1678   result:&:duplex-list:char <- next start
1679   remove-between start, end
1680   # adjust cursor
1681   before-cursor <- copy start
1682   *editor <- put *editor, before-cursor:offset, before-cursor
1683   left:num <- get *editor, left:offset
1684   *editor <- put *editor, cursor-column:offset, left
1685 ]
1686 
1687 scenario editor-deletes-to-start-of-line-with-ctrl-u-2 [
1688   local-scope
1689   assume-screen 10/width, 5/height
1690   s:text <- new [123
1691 456]
1692   e:&:editor <- new-editor s, 0/left, 10/right
1693   # start on first line (no newline before), press ctrl-u
1694   assume-console [
1695     left-click 1, 2
1696     press ctrl-u
1697   ]
1698   run [
1699     editor-event-loop screen, console, e
1700   ]
1701   # cursor deletes to start of line
1702   screen-should-contain [
1703     .          .
1704     .3         .
1705     .456       .
1706     .╌╌╌╌╌╌╌╌╌╌.
1707     .          .
1708   ]
1709 ]
1710 
1711 scenario editor-deletes-to-start-of-line-with-ctrl-u-3 [
1712   local-scope
1713   assume-screen 10/width, 5/height
1714   s:text <- new [123
1715 456]
1716   e:&:editor <- new-editor s, 0/left, 10/right
1717   # start past end of line, press ctrl-u
1718   assume-console [
1719     left-click 1, 3
1720     press ctrl-u
1721   ]
1722   run [
1723     editor-event-loop screen, console, e
1724   ]
1725   # cursor deletes to start of line
1726   screen-should-contain [
1727     .          .
1728     .          .
1729     .456       .
1730     .╌╌╌╌╌╌╌╌╌╌.
1731     .          .
1732   ]
1733 ]
1734 
1735 scenario editor-deletes-to-start-of-final-line-with-ctrl-u [
1736   local-scope
1737   assume-screen 10/width, 5/height
1738   s:text <- new [123
1739 456]
1740   e:&:editor <- new-editor s, 0/left, 10/right
1741   # start past end of final line, press ctrl-u
1742   assume-console [
1743     left-click 2, 3
1744     press ctrl-u
1745   ]
1746   run [
1747     editor-event-loop screen, console, e
1748   ]
1749   # cursor deletes to start of line
1750   screen-should-contain [
1751     .          .
1752     .123       .
1753     .          .
1754     .╌╌╌╌╌╌╌╌╌╌.
1755     .          .
1756   ]
1757 ]
1758 
1759 # ctrl-k - delete text from cursor to end of line (but not the newline)
1760 
1761 scenario editor-deletes-to-end-of-line-with-ctrl-k [
1762   local-scope
1763   assume-screen 10/width, 5/height
1764   s:text <- new [123
1765 456]
1766   e:&:editor <- new-editor s, 0/left, 10/right
1767   # start on first line, press ctrl-k
1768   assume-console [
1769     left-click 1, 1
1770     press ctrl-k
1771   ]
1772   run [
1773     editor-event-loop screen, console, e
1774   ]
1775   # cursor deletes to end of line
1776   screen-should-contain [
1777     .          .
1778     .1         .
1779     .456       .
1780     .╌╌╌╌╌╌╌╌╌╌.
1781     .          .
1782   ]
1783 ]
1784 
1785 after <handle-special-character> [
1786   {
1787     delete-to-end-of-line?:bool <- equal c, 11/ctrl-k
1788     break-unless delete-to-end-of-line?
1789     <delete-to-end-of-line-begin>
1790     deleted-cells:&:duplex-list:char <- delete-to-end-of-line editor
1791     <delete-to-end-of-line-end>
1792     return 1/go-render
1793   }
1794 ]
1795 
1796 def delete-to-end-of-line editor:&:editor -> result:&:duplex-list:char, editor:&:editor [
1797   local-scope
1798   load-ingredients
1799   # compute range to delete
1800   start:&:duplex-list:char <- get *editor, before-cursor:offset
1801   end:&:duplex-list:char <- next start
1802   {
1803     at-end-of-text?:bool <- equal end, 0/null
1804     break-if at-end-of-text?
1805     curr:char <- get *end, value:offset
1806     at-end-of-line?:bool <- equal curr, 10/newline
1807     break-if at-end-of-line?
1808     end <- next end
1809     loop
1810   }
1811   # snip it out
1812   result <- next start
1813   remove-between start, end
1814 ]
1815 
1816 scenario editor-deletes-to-end-of-line-with-ctrl-k-2 [
1817   local-scope
1818   assume-screen 10/width, 5/height
1819   s:text <- new [123
1820 456]
1821   e:&:editor <- new-editor s, 0/left, 10/right
1822   # start on second line (no newline after), press ctrl-k
1823   assume-console [
1824     left-click 2, 1
1825     press ctrl-k
1826   ]
1827   run [
1828     editor-event-loop screen, console, e
1829   ]
1830   # cursor deletes to end of line
1831   screen-should-contain [
1832     .          .
1833     .123       .
1834     .4         .
1835     .╌╌╌╌╌╌╌╌╌╌.
1836     .          .
1837   ]
1838 ]
1839 
1840 scenario editor-deletes-to-end-of-line-with-ctrl-k-3 [
1841   local-scope
1842   assume-screen 10/width, 5/height
1843   s:text <- new [123
1844 456]
1845   e:&:editor <- new-editor s, 0/left, 10/right
1846   # start at end of line
1847   assume-console [
1848     left-click 1, 2
1849     press ctrl-k
1850   ]
1851   run [
1852     editor-event-loop screen, console, e
1853   ]
1854   # cursor deletes just last character
1855   screen-should-contain [
1856     .          .
1857     .12        .
1858     .456       .
1859     .╌╌╌╌╌╌╌╌╌╌.
1860     .          .
1861   ]
1862 ]
1863 
1864 scenario editor-deletes-to-end-of-line-with-ctrl-k-4 [
1865   local-scope
1866   assume-screen 10/width, 5/height
1867   s:text <- new [123
1868 456]
1869   e:&:editor <- new-editor s, 0/left, 10/right
1870   # start past end of line
1871   assume-console [
1872     left-click 1, 3
1873     press ctrl-k
1874   ]
1875   run [
1876     editor-event-loop screen, console, e
1877   ]
1878   # cursor deletes nothing
1879   screen-should-contain [
1880     .          .
1881     .123       .
1882     .456       .
1883     .╌╌╌╌╌╌╌╌╌╌.
1884     .          .
1885   ]
1886 ]
1887 
1888 scenario editor-deletes-to-end-of-line-with-ctrl-k-5 [
1889   local-scope
1890   assume-screen 10/width, 5/height
1891   s:text <- new [123
1892 456]
1893   e:&:editor <- new-editor s, 0/left, 10/right
1894   # start at end of text
1895   assume-console [
1896     left-click 2, 2
1897     press ctrl-k
1898   ]
1899   run [
1900     editor-event-loop screen, console, e
1901   ]
1902   # cursor deletes just the final character
1903   screen-should-contain [
1904     .          .
1905     .123       .
1906     .45        .
1907     .╌╌╌╌╌╌╌╌╌╌.
1908     .          .
1909   ]
1910 ]
1911 
1912 scenario editor-deletes-to-end-of-line-with-ctrl-k-6 [
1913   local-scope
1914   assume-screen 10/width, 5/height
1915   s:text <- new [123
1916 456]
1917   e:&:editor <- new-editor s, 0/left, 10/right
1918   # start past end of text
1919   assume-console [
1920     left-click 2, 3
1921     press ctrl-k
1922   ]
1923   run [
1924     editor-event-loop screen, console, e
1925   ]
1926   # cursor deletes nothing
1927   screen-should-contain [
1928     .          .
1929     .123       .
1930     .456       .
1931     .╌╌╌╌╌╌╌╌╌╌.
1932     .          .
1933   ]
1934 ]
1935 
1936 # cursor-down can scroll if necessary
1937 
1938 scenario editor-can-scroll-down-using-arrow-keys [
1939   local-scope
1940   # screen has 1 line for menu + 3 lines
1941   assume-screen 10/width, 4/height
1942   # initialize editor with >3 lines
1943   s:text <- new [a
1944 b
1945 c
1946 d]
1947   e:&:editor <- new-editor s, 0/left, 10/right
1948   editor-render screen, e
1949   screen-should-contain [
1950     .          .
1951     .a         .
1952     .b         .
1953     .c         .
1954   ]
1955   # position cursor at last line, then try to move further down
1956   assume-console [
1957     left-click 3, 0
1958     press down-arrow
1959   ]
1960   run [
1961     editor-event-loop screen, console, e
1962   ]
1963   # screen slides by one line
1964   screen-should-contain [
1965     .          .
1966     .b         .
1967     .c         .
1968     .d         .
1969   ]
1970 ]
1971 
1972 after <scroll-down> [
1973   trace 10, [app], [scroll down]
1974   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
1975   left:num <- get *editor, left:offset
1976   right:num <- get *editor, right:offset
1977   max:num <- subtract right, left
1978   old-top:&:duplex-list:char <- copy top-of-screen
1979   top-of-screen <- before-start-of-next-line top-of-screen, max
1980   *editor <- put *editor, top-of-screen:offset, top-of-screen
1981   no-movement?:bool <- equal old-top, top-of-screen
1982   return-if no-movement?, 0/don't-render
1983 ]
1984 
1985 # takes a pointer into the doubly-linked list, scans ahead at most 'max'
1986 # positions until the next newline
1987 # beware: never return null pointer.
1988 def before-start-of-next-line original:&:duplex-list:char, max:num -> curr:&:duplex-list:char [
1989   local-scope
1990   load-ingredients
1991   count:num <- copy 0
1992   curr:&:duplex-list:char <- copy original
1993   # skip the initial newline if it exists
1994   {
1995     c:char <- get *curr, value:offset
1996     at-newline?:bool <- equal c, 10/newline
1997     break-unless at-newline?
1998     curr <- next curr
1999     count <- add count, 1
2000   }
2001   {
2002     return-unless curr, original
2003     done?:bool <- greater-or-equal count, max
2004     break-if done?
2005     c:char <- get *curr, value:offset
2006     at-newline?:bool <- equal c, 10/newline
2007     break-if at-newline?
2008     curr <- next curr
2009     count <- add count, 1
2010     loop
2011   }
2012   return-unless curr, original
2013   return curr
2014 ]
2015 
2016 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys [
2017   local-scope
2018   # screen has 1 line for menu + 3 lines
2019   assume-screen 10/width, 4/height
2020   # initialize editor with a long, wrapped line and more than a screen of
2021   # other lines
2022   s:text <- new [abcdef
2023 g
2024 h
2025 i]
2026   e:&:editor <- new-editor s, 0/left, 5/right
2027   editor-render screen, e
2028   screen-should-contain [
2029     .          .
2030     .abcd↩     .
2031     .ef        .
2032     .g         .
2033   ]
2034   # position cursor at last line, then try to move further down
2035   assume-console [
2036     left-click 3, 0
2037     press down-arrow
2038   ]
2039   run [
2040     editor-event-loop screen, console, e
2041   ]
2042   # screen shows partial wrapped line
2043   screen-should-contain [
2044     .          .
2045     .ef        .
2046     .g         .
2047     .h         .
2048   ]
2049 ]
2050 
2051 scenario editor-scrolls-down-past-wrapped-line-using-arrow-keys-2 [
2052   local-scope
2053   # screen has 1 line for menu + 3 lines
2054   assume-screen 10/width, 4/height
2055   # editor starts with a long line wrapping twice
2056   s:text <- new [abcdefghij
2057 k
2058 l
2059 m]
2060   e:&:editor <- new-editor s, 0/left, 5/right
2061   # position cursor at last line, then try to move further down
2062   assume-console [
2063     left-click 3, 0
2064     press down-arrow
2065   ]
2066   run [
2067     editor-event-loop screen, console, e
2068   ]
2069   # screen shows partial wrapped line containing a wrap icon
2070   screen-should-contain [
2071     .          .
2072     .efgh↩     .
2073     .ij        .
2074     .k         .
2075   ]
2076   # scroll down again
2077   assume-console [
2078     press down-arrow
2079   ]
2080   run [
2081     editor-event-loop screen, console, e
2082   ]
2083   # screen shows partial wrapped line
2084   screen-should-contain [
2085     .          .
2086     .ij        .
2087     .k         .
2088     .l         .
2089   ]
2090 ]
2091 
2092 scenario editor-scrolls-down-when-line-wraps [
2093   local-scope
2094   # screen has 1 line for menu + 3 lines
2095   assume-screen 5/width, 4/height
2096   # editor contains a long line in the third line
2097   s:text <- new [a
2098 b
2099 cdef]
2100   e:&:editor <- new-editor s, 0/left, 5/right
2101   # position cursor at end, type a character
2102   assume-console [
2103     left-click 3, 4
2104     type [g]
2105   ]
2106   run [
2107     editor-event-loop screen, console, e
2108     3:num/raw <- get *e, cursor-row:offset
2109     4:num/raw <- get *e, cursor-column:offset
2110   ]
2111   # screen scrolls
2112   screen-should-contain [
2113     .     .
2114     .b    .
2115     .cdef↩.
2116     .g    .
2117   ]
2118   memory-should-contain [
2119     3 <- 3
2120     4 <- 1
2121   ]
2122 ]
2123 
2124 scenario editor-scrolls-down-on-newline [
2125   local-scope
2126   assume-screen 5/width, 4/height
2127   # position cursor after last line and type newline
2128   s:text <- new [a
2129 b
2130 c]
2131   e:&:editor <- new-editor s, 0/left, 5/right
2132   assume-console [
2133     left-click 3, 4
2134     type [
2135 ]
2136   ]
2137   run [
2138     editor-event-loop screen, console, e
2139     3:num/raw <- get *e, cursor-row:offset
2140     4:num/raw <- get *e, cursor-column:offset
2141   ]
2142   # screen scrolls
2143   screen-should-contain [
2144     .     .
2145     .b    .
2146     .c    .
2147     .     .
2148   ]
2149   memory-should-contain [
2150     3 <- 3
2151     4 <- 0
2152   ]
2153 ]
2154 
2155 scenario editor-scrolls-down-on-right-arrow [
2156   local-scope
2157   # screen has 1 line for menu + 3 lines
2158   assume-screen 5/width, 4/height
2159   # editor contains a wrapped line
2160   s:text <- new [a
2161 b
2162 cdefgh]
2163   e:&:editor <- new-editor s, 0/left, 5/right
2164   # position cursor at end of screen and try to move right
2165   assume-console [
2166     left-click 3, 3
2167     press right-arrow
2168   ]
2169   run [
2170     editor-event-loop screen, console, e
2171     3:num/raw <- get *e, cursor-row:offset
2172     4:num/raw <- get *e, cursor-column:offset
2173   ]
2174   # screen scrolls
2175   screen-should-contain [
2176     .     .
2177     .b    .
2178     .cdef↩.
2179     .gh   .
2180   ]
2181   memory-should-contain [
2182     3 <- 3
2183     4 <- 0
2184   ]
2185 ]
2186 
2187 scenario editor-scrolls-down-on-right-arrow-2 [
2188   local-scope
2189   # screen has 1 line for menu + 3 lines
2190   assume-screen 5/width, 4/height
2191   # editor contains more lines than can fit on screen
2192   s:text <- new [a
2193 b
2194 c
2195 d]
2196   e:&:editor <- new-editor s, 0/left, 5/right
2197   # position cursor at end of screen and try to move right
2198   assume-console [
2199     left-click 3, 3
2200     press right-arrow
2201   ]
2202   run [
2203     editor-event-loop screen, console, e
2204     3:num/raw <- get *e, cursor-row:offset
2205     4:num/raw <- get *e, cursor-column:offset
2206   ]
2207   # screen scrolls
2208   screen-should-contain [
2209     .     .
2210     .b    .
2211     .c    .
2212     .d    .
2213   ]
2214   memory-should-contain [
2215     3 <- 3
2216     4 <- 0
2217   ]
2218 ]
2219 
2220 scenario editor-scrolls-at-end-on-down-arrow [
2221   local-scope
2222   assume-screen 10/width, 5/height
2223   s:text <- new [abc
2224 de]
2225   e:&:editor <- new-editor s, 0/left, 10/right
2226   editor-render screen, e
2227   $clear-trace
2228   # try to move down past end of text
2229   assume-console [
2230     left-click 2, 0
2231     press down-arrow
2232   ]
2233   run [
2234     editor-event-loop screen, console, e
2235     3:num/raw <- get *e, cursor-row:offset
2236     4:num/raw <- get *e, cursor-column:offset
2237   ]
2238   # screen should scroll, moving cursor to end of text
2239   memory-should-contain [
2240     3 <- 1
2241     4 <- 2
2242   ]
2243   assume-console [
2244     type [0]
2245   ]
2246   run [
2247     editor-event-loop screen, console, e
2248   ]
2249   screen-should-contain [
2250     .          .
2251     .de0       .
2252     .╌╌╌╌╌╌╌╌╌╌.
2253     .          .
2254   ]
2255   # try to move down again
2256   $clear-trace
2257   assume-console [
2258     left-click 2, 0
2259     press down-arrow
2260   ]
2261   run [
2262     editor-event-loop screen, console, e
2263     3:num/raw <- get *e, cursor-row:offset
2264     4:num/raw <- get *e, cursor-column:offset
2265   ]
2266   # screen stops scrolling because cursor is already at top
2267   memory-should-contain [
2268     3 <- 1
2269     4 <- 3
2270   ]
2271   check-trace-count-for-label 0, [print-character]
2272   assume-console [
2273     type [1]
2274   ]
2275   run [
2276     editor-event-loop screen, console, e
2277   ]
2278   screen-should-contain [
2279     .          .
2280     .de01      .
2281     .╌╌╌╌╌╌╌╌╌╌.
2282     .          .
2283   ]
2284 ]
2285 
2286 scenario editor-combines-page-and-line-scroll [
2287   local-scope
2288   # screen has 1 line for menu + 3 lines
2289   assume-screen 10/width, 4/height
2290   # initialize editor with a few pages of lines
2291   s:text <- new [a
2292 b
2293 c
2294 d
2295 e
2296 f
2297 g]
2298   e:&:editor <- new-editor s, 0/left, 5/right
2299   editor-render screen, e
2300   # scroll down one page and one line
2301   assume-console [
2302     press page-down
2303     left-click 3, 0
2304     press down-arrow
2305   ]
2306   run [
2307     editor-event-loop screen, console, e
2308   ]
2309   # screen scrolls down 3 lines
2310   screen-should-contain [
2311     .          .
2312     .d         .
2313     .e         .
2314     .f         .
2315   ]
2316 ]
2317 
2318 # cursor-up can scroll if necessary
2319 
2320 scenario editor-can-scroll-up-using-arrow-keys [
2321   local-scope
2322   # screen has 1 line for menu + 3 lines
2323   assume-screen 10/width, 4/height
2324   # initialize editor with >3 lines
2325   s:text <- new [a
2326 b
2327 c
2328 d]
2329   e:&:editor <- new-editor s, 0/left, 10/right
2330   editor-render screen, e
2331   screen-should-contain [
2332     .          .
2333     .a         .
2334     .b         .
2335     .c         .
2336   ]
2337   # position cursor at top of second page, then try to move up
2338   assume-console [
2339     press page-down
2340     press up-arrow
2341   ]
2342   run [
2343     editor-event-loop screen, console, e
2344   ]
2345   # screen slides by one line
2346   screen-should-contain [
2347     .          .
2348     .b         .
2349     .c         .
2350     .d         .
2351   ]
2352 ]
2353 
2354 after <scroll-up> [
2355   trace 10, [app], [scroll up]
2356   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2357   old-top:&:duplex-list:char <- copy top-of-screen
2358   top-of-screen <- before-previous-line top-of-screen, editor
2359   *editor <- put *editor, top-of-screen:offset, top-of-screen
2360   no-movement?:bool <- equal old-top, top-of-screen
2361   return-if no-movement?, 0/don't-render
2362 ]
2363 
2364 # takes a pointer into the doubly-linked list, scans back to before start of
2365 # previous *wrapped* line
2366 # beware: never return null pointer
2367 def before-previous-line in:&:duplex-list:char, editor:&:editor -> out:&:duplex-list:char [
2368   local-scope
2369   load-ingredients
2370   curr:&:duplex-list:char <- copy in
2371   c:char <- get *curr, value:offset
2372   # compute max, number of characters to skip
2373   #   1 + len%(width-1)
2374   #   except rotate second term to vary from 1 to width-1 rather than 0 to width-2
2375   left:num <- get *editor, left:offset
2376   right:num <- get *editor, right:offset
2377   max-line-length:num <- subtract right, left, -1/exclusive-right, 1/wrap-icon
2378   sentinel:&:duplex-list:char <- get *editor, data:offset
2379   len:num <- previous-line-length curr, sentinel
2380   {
2381     break-if len
2382     # empty line; just skip this newline
2383     prev:&:duplex-list:char <- prev curr
2384     return-unless prev, curr
2385     return prev
2386   }
2387   _, max:num <- divide-with-remainder len, max-line-length
2388   # remainder 0 => scan one width-worth
2389   {
2390     break-if max
2391     max <- copy max-line-length
2392   }
2393   max <- add max, 1
2394   count:num <- copy 0
2395   # skip 'max' characters
2396   {
2397     done?:bool <- greater-or-equal count, max
2398     break-if done?
2399     prev:&:duplex-list:char <- prev curr
2400     break-unless prev
2401     curr <- copy prev
2402     count <- add count, 1
2403     loop
2404   }
2405   return curr
2406 ]
2407 
2408 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys [
2409   local-scope
2410   # screen has 1 line for menu + 3 lines
2411   assume-screen 10/width, 4/height
2412   # initialize editor with a long, wrapped line and more than a screen of
2413   # other lines
2414   s:text <- new [abcdef
2415 g
2416 h
2417 i]
2418   e:&:editor <- new-editor s, 0/left, 5/right
2419   editor-render screen, e
2420   screen-should-contain [
2421     .          .
2422     .abcd↩     .
2423     .ef        .
2424     .g         .
2425   ]
2426   # position cursor at top of second page, just below wrapped line
2427   assume-console [
2428     press page-down
2429   ]
2430   run [
2431     editor-event-loop screen, console, e
2432   ]
2433   screen-should-contain [
2434     .          .
2435     .g         .
2436     .h         .
2437     .i         .
2438   ]
2439   # now move up one line
2440   assume-console [
2441     press up-arrow
2442   ]
2443   run [
2444     editor-event-loop screen, console, e
2445   ]
2446   # screen shows partial wrapped line
2447   screen-should-contain [
2448     .          .
2449     .ef        .
2450     .g         .
2451     .h         .
2452   ]
2453 ]
2454 
2455 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-2 [
2456   local-scope
2457   # screen has 1 line for menu + 4 lines
2458   assume-screen 10/width, 5/height
2459   # editor starts with a long line wrapping twice, occupying 3 of the 4 lines
2460   s:text <- new [abcdefghij
2461 k
2462 l
2463 m]
2464   e:&:editor <- new-editor s, 0/left, 5/right
2465   editor-render screen, e
2466   # position cursor at top of second page
2467   assume-console [
2468     press page-down
2469   ]
2470   run [
2471     editor-event-loop screen, console, e
2472   ]
2473   screen-should-contain [
2474     .          .
2475     .k         .
2476     .l         .
2477     .m         .
2478     .╌╌╌╌╌     .
2479   ]
2480   # move up one line
2481   assume-console [
2482     press up-arrow
2483   ]
2484   run [
2485     editor-event-loop screen, console, e
2486   ]
2487   # screen shows partial wrapped line
2488   screen-should-contain [
2489     .          .
2490     .ij        .
2491     .k         .
2492     .l         .
2493     .m         .
2494   ]
2495   # move up a second line
2496   assume-console [
2497     press up-arrow
2498   ]
2499   run [
2500     editor-event-loop screen, console, e
2501   ]
2502   # screen shows partial wrapped line
2503   screen-should-contain [
2504     .          .
2505     .efgh↩     .
2506     .ij        .
2507     .k         .
2508     .l         .
2509   ]
2510   # move up a third line
2511   assume-console [
2512     press up-arrow
2513   ]
2514   run [
2515     editor-event-loop screen, console, e
2516   ]
2517   # screen shows partial wrapped line
2518   screen-should-contain [
2519     .          .
2520     .abcd↩     .
2521     .efgh↩     .
2522     .ij        .
2523     .k         .
2524   ]
2525 ]
2526 
2527 # same as editor-scrolls-up-past-wrapped-line-using-arrow-keys but length
2528 # slightly off, just to prevent over-training
2529 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-3 [
2530   local-scope
2531   # screen has 1 line for menu + 3 lines
2532   assume-screen 10/width, 4/height
2533   # initialize editor with a long, wrapped line and more than a screen of
2534   # other lines
2535   s:text <- new [abcdef
2536 g
2537 h
2538 i]
2539   e:&:editor <- new-editor s, 0/left, 6/right
2540   editor-render screen, e
2541   screen-should-contain [
2542     .          .
2543     .abcde↩    .
2544     .f         .
2545     .g         .
2546   ]
2547   # position cursor at top of second page, just below wrapped line
2548   assume-console [
2549     press page-down
2550   ]
2551   run [
2552     editor-event-loop screen, console, e
2553   ]
2554   screen-should-contain [
2555     .          .
2556     .g         .
2557     .h         .
2558     .i         .
2559   ]
2560   # now move up one line
2561   assume-console [
2562     press up-arrow
2563   ]
2564   run [
2565     editor-event-loop screen, console, e
2566   ]
2567   # screen shows partial wrapped line
2568   screen-should-contain [
2569     .          .
2570     .f         .
2571     .g         .
2572     .h         .
2573   ]
2574 ]
2575 
2576 # check empty lines
2577 scenario editor-scrolls-up-past-wrapped-line-using-arrow-keys-4 [
2578   local-scope
2579   assume-screen 10/width, 4/height
2580   # initialize editor with some lines around an empty line
2581   s:text <- new [a
2582 b
2583 
2584 c
2585 d
2586 e]
2587   e:&:editor <- new-editor s, 0/left, 6/right
2588   editor-render screen, e
2589   assume-console [
2590     press page-down
2591   ]
2592   run [
2593     editor-event-loop screen, console, e
2594   ]
2595   screen-should-contain [
2596     .          .
2597     .          .
2598     .c         .
2599     .d         .
2600   ]
2601   assume-console [
2602     press page-down
2603   ]
2604   run [
2605     editor-event-loop screen, console, e
2606   ]
2607   screen-should-contain [
2608     .          .
2609     .d         .
2610     .e         .
2611     .╌╌╌╌╌╌    .
2612   ]
2613   assume-console [
2614     press page-up
2615   ]
2616   run [
2617     editor-event-loop screen, console, e
2618   ]
2619   screen-should-contain [
2620     .          .
2621     .          .
2622     .c         .
2623     .d         .
2624   ]
2625 ]
2626 
2627 scenario editor-scrolls-up-on-left-arrow [
2628   local-scope
2629   # screen has 1 line for menu + 3 lines
2630   assume-screen 5/width, 4/height
2631   # editor contains >3 lines
2632   s:text <- new [a
2633 b
2634 c
2635 d
2636 e]
2637   e:&:editor <- new-editor s, 0/left, 5/right
2638   editor-render screen, e
2639   # position cursor at top of second page
2640   assume-console [
2641     press page-down
2642   ]
2643   run [
2644     editor-event-loop screen, console, e
2645   ]
2646   screen-should-contain [
2647     .     .
2648     .c    .
2649     .d    .
2650     .e    .
2651   ]
2652   # now try to move left
2653   assume-console [
2654     press left-arrow
2655   ]
2656   run [
2657     editor-event-loop screen, console, e
2658     3:num/raw <- get *e, cursor-row:offset
2659     4:num/raw <- get *e, cursor-column:offset
2660   ]
2661   # screen scrolls
2662   screen-should-contain [
2663     .     .
2664     .b    .
2665     .c    .
2666     .d    .
2667   ]
2668   memory-should-contain [
2669     3 <- 1
2670     4 <- 1
2671   ]
2672 ]
2673 
2674 scenario editor-can-scroll-up-to-start-of-file [
2675   local-scope
2676   # screen has 1 line for menu + 3 lines
2677   assume-screen 10/width, 4/height
2678   # initialize editor with >3 lines
2679   s:text <- new [a
2680 b
2681 c
2682 d]
2683   e:&:editor <- new-editor s, 0/left, 10/right
2684   editor-render screen, e
2685   screen-should-contain [
2686     .          .
2687     .a         .
2688     .b         .
2689     .c         .
2690   ]
2691   # position cursor at top of second page, then try to move up to start of
2692   # text
2693   assume-console [
2694     press page-down
2695     press up-arrow
2696     press up-arrow
2697   ]
2698   run [
2699     editor-event-loop screen, console, e
2700   ]
2701   # screen slides by one line
2702   screen-should-contain [
2703     .          .
2704     .a         .
2705     .b         .
2706     .c         .
2707   ]
2708   # try to move up again
2709   assume-console [
2710     press up-arrow
2711   ]
2712   run [
2713     editor-event-loop screen, console, e
2714   ]
2715   # screen remains unchanged
2716   screen-should-contain [
2717     .          .
2718     .a         .
2719     .b         .
2720     .c         .
2721   ]
2722 ]
2723 
2724 # ctrl-f/page-down - render next page if it exists
2725 
2726 scenario editor-can-scroll [
2727   local-scope
2728   assume-screen 10/width, 4/height
2729   s:text <- new [a
2730 b
2731 c
2732 d]
2733   e:&:editor <- new-editor s, 0/left, 10/right
2734   editor-render screen, e
2735   screen-should-contain [
2736     .          .
2737     .a         .
2738     .b         .
2739     .c         .
2740   ]
2741   # scroll down
2742   assume-console [
2743     press page-down
2744   ]
2745   run [
2746     editor-event-loop screen, console, e
2747   ]
2748   # screen shows next page
2749   screen-should-contain [
2750     .          .
2751     .c         .
2752     .d         .
2753     .╌╌╌╌╌╌╌╌╌╌.
2754   ]
2755 ]
2756 
2757 after <handle-special-character> [
2758   {
2759     page-down?:bool <- equal c, 6/ctrl-f
2760     break-unless page-down?
2761     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
2762     <move-cursor-begin>
2763     page-down editor
2764     undo-coalesce-tag:num <- copy 0/never
2765     <move-cursor-end>
2766     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2767     movement?:bool <- not-equal top-of-screen, old-top
2768     return movement?/go-render
2769   }
2770 ]
2771 
2772 after <handle-special-key> [
2773   {
2774     page-down?:bool <- equal k, 65518/page-down
2775     break-unless page-down?
2776     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
2777     <move-cursor-begin>
2778     page-down editor
2779     undo-coalesce-tag:num <- copy 0/never
2780     <move-cursor-end>
2781     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2782     movement?:bool <- not-equal top-of-screen, old-top
2783     return movement?/go-render
2784   }
2785 ]
2786 
2787 # page-down skips entire wrapped lines, so it can't scroll past lines
2788 # taking up the entire screen
2789 def page-down editor:&:editor -> editor:&:editor [
2790   local-scope
2791   load-ingredients
2792   # if editor contents don't overflow screen, do nothing
2793   bottom-of-screen:&:duplex-list:char <- get *editor, bottom-of-screen:offset
2794   return-unless bottom-of-screen
2795   # if not, position cursor at final character
2796   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
2797   before-cursor:&:duplex-list:char <- prev bottom-of-screen
2798   *editor <- put *editor, before-cursor:offset, before-cursor
2799   # keep one line in common with previous page
2800   {
2801     last:char <- get *before-cursor, value:offset
2802     newline?:bool <- equal last, 10/newline
2803     break-unless newline?:bool
2804     before-cursor <- prev before-cursor
2805     *editor <- put *editor, before-cursor:offset, before-cursor
2806   }
2807   # move cursor and top-of-screen to start of that line
2808   move-to-start-of-line editor
2809   before-cursor <- get *editor, before-cursor:offset
2810   *editor <- put *editor, top-of-screen:offset, before-cursor
2811 ]
2812 
2813 scenario editor-does-not-scroll-past-end [
2814   local-scope
2815   assume-screen 10/width, 4/height
2816   s:text <- new [a
2817 b]
2818   e:&:editor <- new-editor s, 0/left, 10/right
2819   editor-render screen, e
2820   screen-should-contain [
2821     .          .
2822     .a         .
2823     .b         .
2824     .╌╌╌╌╌╌╌╌╌╌.
2825   ]
2826   # scroll down
2827   assume-console [
2828     press page-down
2829   ]
2830   run [
2831     editor-event-loop screen, console, e
2832   ]
2833   # screen remains unmodified
2834   screen-should-contain [
2835     .          .
2836     .a         .
2837     .b         .
2838     .╌╌╌╌╌╌╌╌╌╌.
2839   ]
2840 ]
2841 
2842 scenario editor-starts-next-page-at-start-of-wrapped-line [
2843   local-scope
2844   # screen has 1 line for menu + 3 lines for text
2845   assume-screen 10/width, 4/height
2846   # editor contains a long last line
2847   s:text <- new [a
2848 b
2849 cdefgh]
2850   # editor screen triggers wrap of last line
2851   e:&:editor <- new-editor s, 0/left, 4/right
2852   editor-render screen, e
2853   # some part of last line is not displayed
2854   screen-should-contain [
2855     .          .
2856     .a         .
2857     .b         .
2858     .cde↩      .
2859   ]
2860   # scroll down
2861   assume-console [
2862     press page-down
2863   ]
2864   run [
2865     editor-event-loop screen, console, e
2866   ]
2867   # screen shows entire wrapped line
2868   screen-should-contain [
2869     .          .
2870     .cde↩      .
2871     .fgh       .
2872     .╌╌╌╌      .
2873   ]
2874 ]
2875 
2876 scenario editor-starts-next-page-at-start-of-wrapped-line-2 [
2877   local-scope
2878   # screen has 1 line for menu + 3 lines for text
2879   assume-screen 10/width, 4/height
2880   # editor contains a very long line that occupies last two lines of screen
2881   # and still has something left over
2882   s:text <- new [a
2883 bcdefgh]
2884   e:&:editor <- new-editor s, 0/left, 4/right
2885   editor-render screen, e
2886   # some part of last line is not displayed
2887   screen-should-contain [
2888     .          .
2889     .a         .
2890     .bcd↩      .
2891     .efg↩      .
2892   ]
2893   # scroll down
2894   assume-console [
2895     press page-down
2896   ]
2897   run [
2898     editor-event-loop screen, console, e
2899   ]
2900   # screen shows entire wrapped line
2901   screen-should-contain [
2902     .          .
2903     .bcd↩      .
2904     .efg↩      .
2905     .h         .
2906   ]
2907 ]
2908 
2909 # ctrl-b/page-up - render previous page if it exists
2910 
2911 scenario editor-can-scroll-up [
2912   local-scope
2913   assume-screen 10/width, 4/height
2914   s:text <- new [a
2915 b
2916 c
2917 d]
2918   e:&:editor <- new-editor s, 0/left, 10/right
2919   editor-render screen, e
2920   screen-should-contain [
2921     .          .
2922     .a         .
2923     .b         .
2924     .c         .
2925   ]
2926   # scroll down
2927   assume-console [
2928     press page-down
2929   ]
2930   run [
2931     editor-event-loop screen, console, e
2932   ]
2933   # screen shows next page
2934   screen-should-contain [
2935     .          .
2936     .c         .
2937     .d         .
2938     .╌╌╌╌╌╌╌╌╌╌.
2939   ]
2940   # scroll back up
2941   assume-console [
2942     press page-up
2943   ]
2944   run [
2945     editor-event-loop screen, console, e
2946   ]
2947   # screen shows original page again
2948   screen-should-contain [
2949     .          .
2950     .a         .
2951     .b         .
2952     .c         .
2953   ]
2954 ]
2955 
2956 after <handle-special-character> [
2957   {
2958     page-up?:bool <- equal c, 2/ctrl-b
2959     break-unless page-up?
2960     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
2961     <move-cursor-begin>
2962     editor <- page-up editor, screen-height
2963     undo-coalesce-tag:num <- copy 0/never
2964     <move-cursor-end>
2965     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2966     movement?:bool <- not-equal top-of-screen, old-top
2967     return movement?/go-render
2968   }
2969 ]
2970 
2971 after <handle-special-key> [
2972   {
2973     page-up?:bool <- equal k, 65519/page-up
2974     break-unless page-up?
2975     old-top:&:duplex-list:char <- get *editor, top-of-screen:offset
2976     <move-cursor-begin>
2977     editor <- page-up editor, screen-height
2978     undo-coalesce-tag:num <- copy 0/never
2979     <move-cursor-end>
2980     top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2981     movement?:bool <- not-equal top-of-screen, old-top
2982     # don't bother re-rendering if nothing changed. todo: test this
2983     return movement?/go-render
2984   }
2985 ]
2986 
2987 def page-up editor:&:editor, screen-height:num -> editor:&:editor [
2988   local-scope
2989   load-ingredients
2990   max:num <- subtract screen-height, 1/menu-bar, 1/overlapping-line
2991   count:num <- copy 0
2992   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
2993   {
2994     done?:bool <- greater-or-equal count, max
2995     break-if done?
2996     prev:&:duplex-list:char <- before-previous-line top-of-screen, editor
2997     break-unless prev
2998     top-of-screen <- copy prev
2999     *editor <- put *editor, top-of-screen:offset, top-of-screen
3000     count <- add count, 1
3001     loop
3002   }
3003 ]
3004 
3005 scenario editor-can-scroll-up-multiple-pages [
3006   local-scope
3007   # screen has 1 line for menu + 3 lines
3008   assume-screen 10/width, 4/height
3009   # initialize editor with 8 lines
3010   s:text <- new [a
3011 b
3012 c
3013 d
3014 e
3015 f
3016 g
3017 h]
3018   e:&:editor <- new-editor s, 0/left, 10/right
3019   editor-render screen, e
3020   screen-should-contain [
3021     .          .
3022     .a         .
3023     .b         .
3024     .c         .
3025   ]
3026   # scroll down two pages
3027   assume-console [
3028     press page-down
3029     press page-down
3030   ]
3031   run [
3032     editor-event-loop screen, console, e
3033   ]
3034   # screen shows third page
3035   screen-should-contain [
3036     .          .
3037     .e         .
3038     .f         .
3039     .g         .
3040   ]
3041   # scroll up
3042   assume-console [
3043     press page-up
3044   ]
3045   run [
3046     editor-event-loop screen, console, e
3047   ]
3048   # screen shows second page
3049   screen-should-contain [
3050     .          .
3051     .c         .
3052     .d         .
3053     .e         .
3054   ]
3055   # scroll up again
3056   assume-console [
3057     press page-up
3058   ]
3059   run [
3060     editor-event-loop screen, console, e
3061   ]
3062   # screen shows original page again
3063   screen-should-contain [
3064     .          .
3065     .a         .
3066     .b         .
3067     .c         .
3068   ]
3069 ]
3070 
3071 scenario editor-can-scroll-up-wrapped-lines [
3072   local-scope
3073   # screen has 1 line for menu + 5 lines for text
3074   assume-screen 10/width, 6/height
3075   # editor contains a long line in the first page
3076   s:text <- new [a
3077 b
3078 cdefgh
3079 i
3080 j
3081 k
3082 l
3083 m
3084 n
3085 o]
3086   # editor screen triggers wrap of last line
3087   e:&:editor <- new-editor s, 0/left, 4/right
3088   editor-render screen, e
3089   # some part of last line is not displayed
3090   screen-should-contain [
3091     .          .
3092     .a         .
3093     .b         .
3094     .cde↩      .
3095     .fgh       .
3096     .i         .
3097   ]
3098   # scroll down a page and a line
3099   assume-console [
3100     press page-down
3101     left-click 5, 0
3102     press down-arrow
3103   ]
3104   run [
3105     editor-event-loop screen, console, e
3106   ]
3107   # screen shows entire wrapped line
3108   screen-should-contain [
3109     .          .
3110     .j         .
3111     .k         .
3112     .l         .
3113     .m         .
3114     .n         .
3115   ]
3116   # now scroll up one page
3117   assume-console [
3118     press page-up
3119   ]
3120   run [
3121     editor-event-loop screen, console, e
3122   ]
3123   # screen resets
3124   screen-should-contain [
3125     .          .
3126     .b         .
3127     .cde↩      .
3128     .fgh       .
3129     .i         .
3130     .j         .
3131   ]
3132 ]
3133 
3134 scenario editor-can-scroll-up-wrapped-lines-2 [
3135   local-scope
3136   # screen has 1 line for menu + 3 lines for text
3137   assume-screen 10/width, 4/height
3138   # editor contains a very long line that occupies last two lines of screen
3139   # and still has something left over
3140   s:text <- new [a
3141 bcdefgh]
3142   e:&:editor <- new-editor s, 0/left, 4/right
3143   editor-render screen, e
3144   # some part of last line is not displayed
3145   screen-should-contain [
3146     .          .
3147     .a         .
3148     .bcd↩      .
3149     .efg↩      .
3150   ]
3151   # scroll down
3152   assume-console [
3153     press page-down
3154   ]
3155   run [
3156     editor-event-loop screen, console, e
3157   ]
3158   # screen shows entire wrapped line
3159   screen-should-contain [
3160     .          .
3161     .bcd↩      .
3162     .efg↩      .
3163     .h         .
3164   ]
3165   # scroll back up
3166   assume-console [
3167     press page-up
3168   ]
3169   run [
3170     editor-event-loop screen, console, e
3171   ]
3172   # screen resets
3173   screen-should-contain [
3174     .          .
3175     .a         .
3176     .bcd↩      .
3177     .efg↩      .
3178   ]
3179 ]
3180 
3181 scenario editor-can-scroll-up-past-nonempty-lines [
3182   local-scope
3183   assume-screen 10/width, 4/height
3184   # text with empty line in second screen
3185   s:text <- new [axx
3186 bxx
3187 cxx
3188 dxx
3189 exx
3190 fxx
3191 gxx
3192 hxx
3193 ]
3194   e:&:editor <- new-editor s, 0/left, 4/right
3195   editor-render screen, e
3196   screen-should-contain [
3197     .          .
3198     .axx       .
3199     .bxx       .
3200     .cxx       .
3201   ]
3202   assume-console [
3203     press page-down
3204   ]
3205   run [
3206     editor-event-loop screen, console, e
3207   ]
3208   screen-should-contain [
3209     .          .
3210     .cxx       .
3211     .dxx       .
3212     .exx       .
3213   ]
3214   assume-console [
3215     press page-down
3216   ]
3217   run [
3218     editor-event-loop screen, console, e
3219   ]
3220   screen-should-contain [
3221     .          .
3222     .exx       .
3223     .fxx       .
3224     .gxx       .
3225   ]
3226   # scroll back up past empty line
3227   assume-console [
3228     press page-up
3229   ]
3230   run [
3231     editor-event-loop screen, console, e
3232   ]
3233   screen-should-contain [
3234     .          .
3235     .cxx       .
3236     .dxx       .
3237     .exx       .
3238   ]
3239 ]
3240 
3241 scenario editor-can-scroll-up-past-empty-lines [
3242   local-scope
3243   assume-screen 10/width, 4/height
3244   # text with empty line in second screen
3245   s:text <- new [axy
3246 bxy
3247 cxy
3248 
3249 dxy
3250 exy
3251 fxy
3252 gxy
3253 ]
3254   e:&:editor <- new-editor s, 0/left, 4/right
3255   editor-render screen, e
3256   screen-should-contain [
3257     .          .
3258     .axy       .
3259     .bxy       .
3260     .cxy       .
3261   ]
3262   assume-console [
3263     press page-down
3264   ]
3265   run [
3266     editor-event-loop screen, console, e
3267   ]
3268   screen-should-contain [
3269     .          .
3270     .cxy       .
3271     .          .
3272     .dxy       .
3273   ]
3274   assume-console [
3275     press page-down
3276   ]
3277   run [
3278     editor-event-loop screen, console, e
3279   ]
3280   screen-should-contain [
3281     .          .
3282     .dxy       .
3283     .exy       .
3284     .fxy       .
3285   ]
3286   # scroll back up past empty line
3287   assume-console [
3288     press page-up
3289   ]
3290   run [
3291     editor-event-loop screen, console, e
3292   ]
3293   screen-should-contain [
3294     .          .
3295     .cxy       .
3296     .          .
3297     .dxy       .
3298   ]
3299 ]