https://github.com/akkartik/mu/blob/master/edit/003-shortcuts.mu

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