1 ## handling events from the keyboard, mouse, touch screen, ...
   2 
   3 # temporary main: interactive editor
   4 # hit ctrl-c to exit
   5 def! main text:text [
   6   local-scope
   7   load-inputs
   8   open-console
   9   clear-screen 0/screen  # non-scrolling app
  10   editor:&:editor <- new-editor text, 5/left, 45/right
  11   editor-render 0/screen, editor
  12   editor-event-loop 0/screen, 0/console, editor
  13   close-console
  14 ]
  15 
  16 def editor-event-loop screen:&:screen, console:&:console, editor:&:editor -> screen:&:screen, console:&:console, editor:&:editor [
  17   local-scope
  18   load-inputs
  19   {
  20   ¦ # looping over each (keyboard or touch) event as it occurs
  21   ¦ +next-event
  22   ¦ cursor-row:num <- get *editor, cursor-row:offset
  23   ¦ cursor-column:num <- get *editor, cursor-column:offset
  24   ¦ screen <- move-cursor screen, cursor-row, cursor-column
  25   ¦ e:event, found?:bool, quit?:bool, console <- read-event console
  26   ¦ loop-unless found?
  27   ¦ break-if quit?  # only in tests
  28   ¦ trace 10, [app], [next-event]
  29   ¦ # 'touch' event
  30   ¦ t:touch-event, is-touch?:bool <- maybe-convert e, touch:variant
  31   ¦ {
  32   ¦ ¦ break-unless is-touch?
  33   ¦ ¦ move-cursor editor, screen, t
  34   ¦ ¦ loop +next-event
  35   ¦ }
  36   ¦ # keyboard events
  37   ¦ {
  38   ¦ ¦ break-if is-touch?
  39   ¦ ¦ go-render?:bool <- handle-keyboard-event screen, editor, e
  40   ¦ ¦ {
  41   ¦ ¦ ¦ break-unless go-render?
  42   ¦ ¦ ¦ screen <- editor-render screen, editor
  43   ¦ ¦ }
  44   ¦ }
  45   ¦ loop
  46   }
  47 ]
  48 
  49 # process click, return if it was on current editor
  50 def move-cursor editor:&:editor, screen:&:screen, t:touch-event -> in-focus?:bool, editor:&:editor [
  51   local-scope
  52   load-inputs
  53   return-unless editor, 0/false
  54   click-row:num <- get t, row:offset
  55   return-unless click-row, 0/false  # ignore clicks on 'menu'
  56   click-column:num <- get t, column:offset
  57   left:num <- get *editor, left:offset
  58   too-far-left?:bool <- lesser-than click-column, left
  59   return-if too-far-left?, 0/false
  60   right:num <- get *editor, right:offset
  61   too-far-right?:bool <- greater-than click-column, right
  62   return-if too-far-right?, 0/false
  63   # position cursor
  64   <begin-move-cursor>
  65   editor <- snap-cursor editor, screen, click-row, click-column
  66   undo-coalesce-tag:num <- copy 0/never
  67   <end-move-cursor>
  68   # gain focus
  69   return 1/true
  70 ]
  71 
  72 # Variant of 'render' that only moves the cursor (coordinates and
  73 # before-cursor). If it's past the end of a line, it 'slides' it left. If it's
  74 # past the last line it positions at end of last line.
  75 def snap-cursor editor:&:editor, screen:&:screen, target-row:num, target-column:num -> editor:&:editor [
  76   local-scope
  77   load-inputs
  78   return-unless editor
  79   left:num <- get *editor, left:offset
  80   right:num <- get *editor, right:offset
  81   screen-height:num <- screen-height screen
  82   # count newlines until screen row
  83   curr:&:duplex-list:char <- get *editor, top-of-screen:offset
  84   prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
  85   curr <- next curr
  86   row:num <- copy 1/top
  87   column:num <- copy left
  88   *editor <- put *editor, cursor-row:offset, target-row
  89   cursor-row:num <- copy target-row
  90   *editor <- put *editor, cursor-column:offset, target-column
  91   cursor-column:num <- copy target-column
  92   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
  93   {
  94   ¦ +next-character
  95   ¦ break-unless curr
  96   ¦ off-screen?:bool <- greater-or-equal row, screen-height
  97   ¦ break-if off-screen?
  98   ¦ # update editor.before-cursor
  99   ¦ # Doing so at the start of each iteration ensures it stays one step behind
 100   ¦ # the current character.
 101   ¦ {
 102   ¦ ¦ at-cursor-row?:bool <- equal row, cursor-row
 103   ¦ ¦ break-unless at-cursor-row?
 104   ¦ ¦ at-cursor?:bool <- equal column, cursor-column
 105   ¦ ¦ break-unless at-cursor?
 106   ¦ ¦ before-cursor <- copy prev
 107   ¦ ¦ *editor <- put *editor, before-cursor:offset, before-cursor
 108   ¦ }
 109   ¦ c:char <- get *curr, value:offset
 110   ¦ {
 111   ¦ ¦ # newline? move to left rather than 0
 112   ¦ ¦ newline?:bool <- equal c, 10/newline
 113   ¦ ¦ break-unless newline?
 114   ¦ ¦ # adjust cursor if necessary
 115   ¦ ¦ {
 116   ¦ ¦ ¦ at-cursor-row?:bool <- equal row, cursor-row
 117   ¦ ¦ ¦ break-unless at-cursor-row?
 118   ¦ ¦ ¦ left-of-cursor?:bool <- lesser-than column, cursor-column
 119   ¦ ¦ ¦ break-unless left-of-cursor?
 120   ¦ ¦ ¦ cursor-column <- copy column
 121   ¦ ¦ ¦ *editor <- put *editor, cursor-column:offset, cursor-column
 122   ¦ ¦ ¦ before-cursor <- copy prev
 123   ¦ ¦ ¦ *editor <- put *editor, before-cursor:offset, before-cursor
 124   ¦ ¦ }
 125   ¦ ¦ # skip to next line
 126   ¦ ¦ row <- add row, 1
 127   ¦ ¦ column <- copy left
 128   ¦ ¦ curr <- next curr
 129   ¦ ¦ prev <- next prev
 130   ¦ ¦ loop +next-character
 131   ¦ }
 132   ¦ {
 133   ¦ ¦ # at right? wrap. even if there's only one more letter left; we need
 134   ¦ ¦ # room for clicking on the cursor after it.
 135   ¦ ¦ at-right?:bool <- equal column, right
 136   ¦ ¦ break-unless at-right?
 137   ¦ ¦ column <- copy left
 138   ¦ ¦ row <- add row, 1
 139   ¦ ¦ # don't increment curr/prev
 140   ¦ ¦ loop +next-character
 141   ¦ }
 142   ¦ curr <- next curr
 143   ¦ prev <- next prev
 144   ¦ column <- add column, 1
 145   ¦ loop
 146   }
 147   # is cursor to the right of the last line? move to end
 148   {
 149   ¦ at-cursor-row?:bool <- equal row, cursor-row
 150   ¦ cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
 151   ¦ before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
 152   ¦ above-cursor-row?:bool <- lesser-than row, cursor-row
 153   ¦ before-cursor?:bool <- or before-cursor-on-same-line?, above-cursor-row?
 154   ¦ break-unless before-cursor?
 155   ¦ cursor-row <- copy row
 156   ¦ *editor <- put *editor, cursor-row:offset, cursor-row
 157   ¦ cursor-column <- copy column
 158   ¦ *editor <- put *editor, cursor-column:offset, cursor-column
 159   ¦ before-cursor <- copy prev
 160   ¦ *editor <- put *editor, before-cursor:offset, before-cursor
 161   }
 162 ]
 163 
 164 # Process an event 'e' and try to minimally update the screen.
 165 # Set 'go-render?' to true to indicate the caller must perform a non-minimal update.
 166 def handle-keyboard-event screen:&:screen, editor:&:editor, e:event -> go-render?:bool, screen:&:screen, editor:&:editor [
 167   local-scope
 168   load-inputs
 169   return-unless editor, 0/don't-render
 170   screen-width:num <- screen-width screen
 171   screen-height:num <- screen-height screen
 172   left:num <- get *editor, left:offset
 173   right:num <- get *editor, right:offset
 174   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 175   cursor-row:num <- get *editor, cursor-row:offset
 176   cursor-column:num <- get *editor, cursor-column:offset
 177   save-row:num <- copy cursor-row
 178   save-column:num <- copy cursor-column
 179   # character
 180   {
 181   ¦ c:char, is-unicode?:bool <- maybe-convert e, text:variant
 182   ¦ break-unless is-unicode?
 183   ¦ trace 10, [app], [handle-keyboard-event: special character]
 184   ¦ # exceptions for special characters go here
 185   ¦ <handle-special-character>
 186   ¦ # ignore any other special characters
 187   ¦ regular-character?:bool <- greater-or-equal c, 32/space
 188   ¦ return-unless regular-character?, 0/don't-render
 189   ¦ # otherwise type it in
 190   ¦ <begin-insert-character>
 191   ¦ go-render? <- insert-at-cursor editor, c, screen
 192   ¦ <end-insert-character>
 193   ¦ return
 194   }
 195   # special key to modify the text or move the cursor
 196   k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
 197   assert is-keycode?, [event was of unknown type; neither keyboard nor mouse]
 198   # handlers for each special key will go here
 199   <handle-special-key>
 200   return 1/go-render
 201 ]
 202 
 203 def insert-at-cursor editor:&:editor, c:char, screen:&:screen -> go-render?:bool, editor:&:editor, screen:&:screen [
 204   local-scope
 205   load-inputs
 206   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 207   insert c, before-cursor
 208   before-cursor <- next before-cursor
 209   *editor <- put *editor, before-cursor:offset, before-cursor
 210   cursor-row:num <- get *editor, cursor-row:offset
 211   cursor-column:num <- get *editor, cursor-column:offset
 212   left:num <- get *editor, left:offset
 213   right:num <- get *editor, right:offset
 214   save-row:num <- copy cursor-row
 215   save-column:num <- copy cursor-column
 216   screen-width:num <- screen-width screen
 217   screen-height:num <- screen-height screen
 218   # occasionally we'll need to mess with the cursor
 219   <insert-character-special-case>
 220   # but mostly we'll just move the cursor right
 221   cursor-column <- add cursor-column, 1
 222   *editor <- put *editor, cursor-column:offset, cursor-column
 223   next:&:duplex-list:char <- next before-cursor
 224   {
 225   ¦ # at end of all text? no need to scroll? just print the character and leave
 226   ¦ at-end?:bool <- equal next, 0/null
 227   ¦ break-unless at-end?
 228   ¦ bottom:num <- subtract screen-height, 1
 229   ¦ at-bottom?:bool <- equal save-row, bottom
 230   ¦ at-right?:bool <- equal save-column, right
 231   ¦ overflow?:bool <- and at-bottom?, at-right?
 232   ¦ break-if overflow?
 233   ¦ move-cursor screen, save-row, save-column
 234   ¦ print screen, c
 235   ¦ return 0/don't-render
 236   }
 237   {
 238   ¦ # not at right margin? print the character and rest of line
 239   ¦ break-unless next
 240   ¦ at-right?:bool <- greater-or-equal cursor-column, screen-width
 241   ¦ break-if at-right?
 242   ¦ curr:&:duplex-list:char <- copy before-cursor
 243   ¦ move-cursor screen, save-row, save-column
 244   ¦ curr-column:num <- copy save-column
 245   ¦ {
 246   ¦ ¦ # hit right margin? give up and let caller render
 247   ¦ ¦ at-right?:bool <- greater-than curr-column, right
 248   ¦ ¦ return-if at-right?, 1/go-render
 249   ¦ ¦ break-unless curr
 250   ¦ ¦ # newline? done.
 251   ¦ ¦ currc:char <- get *curr, value:offset
 252   ¦ ¦ at-newline?:bool <- equal currc, 10/newline
 253   ¦ ¦ break-if at-newline?
 254   ¦ ¦ print screen, currc
 255   ¦ ¦ curr-column <- add curr-column, 1
 256   ¦ ¦ curr <- next curr
 257   ¦ ¦ loop
 258   ¦ }
 259   ¦ return 0/don't-render
 260   }
 261   return 1/go-render
 262 ]
 263 
 264 # helper for tests
 265 def editor-render screen:&:screen, editor:&:editor -> screen:&:screen, editor:&:editor [
 266   local-scope
 267   load-inputs
 268   old-top-idx:num <- save-top-idx screen
 269   left:num <- get *editor, left:offset
 270   right:num <- get *editor, right:offset
 271   row:num, column:num <- render screen, editor
 272   clear-line-until screen, right
 273   row <- add row, 1
 274   draw-horizontal screen, row, left, right, 9480/horizontal-dotted
 275   row <- add row, 1
 276   clear-screen-from screen, row, left, left, right
 277   assert-no-scroll screen, old-top-idx
 278 ]
 279 
 280 scenario editor-handles-empty-event-queue [
 281   local-scope
 282   assume-screen 10/width, 5/height
 283   e:&:editor <- new-editor [abc], 0/left, 10/right
 284   editor-render screen, e
 285   assume-console []
 286   run [
 287   ¦ editor-event-loop screen, console, e
 288   ]
 289   screen-should-contain [
 290   ¦ .          .
 291   ¦ .abc       .
 292   ¦ .╌╌╌╌╌╌╌╌╌╌.
 293   ¦ .          .
 294   ]
 295 ]
 296 
 297 scenario editor-handles-mouse-clicks [
 298   local-scope
 299   assume-screen 10/width, 5/height
 300   e:&:editor <- new-editor [abc], 0/left, 10/right
 301   editor-render screen, e
 302   $clear-trace
 303   assume-console [
 304   ¦ left-click 1, 1  # on the 'b'
 305   ]
 306   run [
 307   ¦ editor-event-loop screen, console, e
 308   ¦ 3:num/raw <- get *e, cursor-row:offset
 309   ¦ 4:num/raw <- get *e, cursor-column:offset
 310   ]
 311   screen-should-contain [
 312   ¦ .          .
 313   ¦ .abc       .
 314   ¦ .╌╌╌╌╌╌╌╌╌╌.
 315   ¦ .          .
 316   ]
 317   memory-should-contain [
 318   ¦ 3 <- 1  # cursor is at row 0..
 319   ¦ 4 <- 1  # ..and column 1
 320   ]
 321   check-trace-count-for-label 0, [print-character]
 322 ]
 323 
 324 scenario editor-handles-mouse-clicks-outside-text [
 325   local-scope
 326   assume-screen 10/width, 5/height
 327   e:&:editor <- new-editor [abc], 0/left, 10/right
 328   $clear-trace
 329   assume-console [
 330   ¦ left-click 1, 7  # last line, to the right of text
 331   ]
 332   run [
 333   ¦ editor-event-loop screen, console, e
 334   ¦ 3:num/raw <- get *e, cursor-row:offset
 335   ¦ 4:num/raw <- get *e, cursor-column:offset
 336   ]
 337   memory-should-contain [
 338   ¦ 3 <- 1  # cursor row
 339   ¦ 4 <- 3  # cursor column
 340   ]
 341   check-trace-count-for-label 0, [print-character]
 342 ]
 343 
 344 scenario editor-handles-mouse-clicks-outside-text-2 [
 345   local-scope
 346   assume-screen 10/width, 5/height
 347   s:text <- new [abc
 348 def]
 349   e:&:editor <- new-editor s, 0/left, 10/right
 350   $clear-trace
 351   assume-console [
 352   ¦ left-click 1, 7  # interior line, to the right of text
 353   ]
 354   run [
 355   ¦ editor-event-loop screen, console, e
 356   ¦ 3:num/raw <- get *e, cursor-row:offset
 357   ¦ 4:num/raw <- get *e, cursor-column:offset
 358   ]
 359   memory-should-contain [
 360   ¦ 3 <- 1  # cursor row
 361   ¦ 4 <- 3  # cursor column
 362   ]
 363   check-trace-count-for-label 0, [print-character]
 364 ]
 365 
 366 scenario editor-handles-mouse-clicks-outside-text-3 [
 367   local-scope
 368   assume-screen 10/width, 5/height
 369   s:text <- new [abc
 370 def]
 371   e:&:editor <- new-editor s, 0/left, 10/right
 372   $clear-trace
 373   assume-console [
 374   ¦ left-click 3, 7  # below text
 375   ]
 376   run [
 377   ¦ editor-event-loop screen, console, e
 378   ¦ 3:num/raw <- get *e, cursor-row:offset
 379   ¦ 4:num/raw <- get *e, cursor-column:offset
 380   ]
 381   memory-should-contain [
 382   ¦ 3 <- 2  # cursor row
 383   ¦ 4 <- 3  # cursor column
 384   ]
 385   check-trace-count-for-label 0, [print-character]
 386 ]
 387 
 388 scenario editor-handles-mouse-clicks-outside-column [
 389   local-scope
 390   assume-screen 10/width, 5/height
 391   # editor occupies only left half of screen
 392   e:&:editor <- new-editor [abc], 0/left, 5/right
 393   editor-render screen, e
 394   $clear-trace
 395   assume-console [
 396   ¦ # click on right half of screen
 397   ¦ left-click 3, 8
 398   ]
 399   run [
 400   ¦ editor-event-loop screen, console, e
 401   ¦ 3:num/raw <- get *e, cursor-row:offset
 402   ¦ 4:num/raw <- get *e, cursor-column:offset
 403   ]
 404   screen-should-contain [
 405   ¦ .          .
 406   ¦ .abc       .
 407   ¦ .╌╌╌╌╌     .
 408   ¦ .          .
 409   ]
 410   memory-should-contain [
 411   ¦ 3 <- 1  # no change to cursor row
 412   ¦ 4 <- 0  # ..or column
 413   ]
 414   check-trace-count-for-label 0, [print-character]
 415 ]
 416 
 417 scenario editor-handles-mouse-clicks-in-menu-area [
 418   local-scope
 419   assume-screen 10/width, 5/height
 420   e:&:editor <- new-editor [abc], 0/left, 5/right
 421   editor-render screen, e
 422   $clear-trace
 423   assume-console [
 424   ¦ # click on first, 'menu' row
 425   ¦ left-click 0, 3
 426   ]
 427   run [
 428   ¦ editor-event-loop screen, console, e
 429   ¦ 3:num/raw <- get *e, cursor-row:offset
 430   ¦ 4:num/raw <- get *e, cursor-column:offset
 431   ]
 432   # no change to cursor
 433   memory-should-contain [
 434   ¦ 3 <- 1
 435   ¦ 4 <- 0
 436   ]
 437 ]
 438 
 439 scenario editor-inserts-characters-into-empty-editor [
 440   local-scope
 441   assume-screen 10/width, 5/height
 442   e:&:editor <- new-editor [], 0/left, 5/right
 443   editor-render screen, e
 444   $clear-trace
 445   assume-console [
 446   ¦ type [abc]
 447   ]
 448   run [
 449   ¦ editor-event-loop screen, console, e
 450   ]
 451   screen-should-contain [
 452   ¦ .          .
 453   ¦ .abc       .
 454   ¦ .╌╌╌╌╌     .
 455   ¦ .          .
 456   ]
 457   check-trace-count-for-label 3, [print-character]
 458 ]
 459 
 460 scenario editor-inserts-characters-at-cursor [
 461   local-scope
 462   assume-screen 10/width, 5/height
 463   e:&:editor <- new-editor [abc], 0/left, 10/right
 464   editor-render screen, e
 465   $clear-trace
 466   # type two letters at different places
 467   assume-console [
 468   ¦ type [0]
 469   ¦ left-click 1, 2
 470   ¦ type [d]
 471   ]
 472   run [
 473   ¦ editor-event-loop screen, console, e
 474   ]
 475   screen-should-contain [
 476   ¦ .          .
 477   ¦ .0adbc     .
 478   ¦ .╌╌╌╌╌╌╌╌╌╌.
 479   ¦ .          .
 480   ]
 481   check-trace-count-for-label 7, [print-character]  # 4 for first letter, 3 for second
 482 ]
 483 
 484 scenario editor-inserts-characters-at-cursor-2 [
 485   local-scope
 486   assume-screen 10/width, 5/height
 487   e:&:editor <- new-editor [abc], 0/left, 10/right
 488   editor-render screen, e
 489   $clear-trace
 490   assume-console [
 491   ¦ left-click 1, 5  # right of last line
 492   ¦ type [d]
 493   ]
 494   run [
 495   ¦ editor-event-loop screen, console, e
 496   ]
 497   screen-should-contain [
 498   ¦ .          .
 499   ¦ .abcd      .
 500   ¦ .╌╌╌╌╌╌╌╌╌╌.
 501   ¦ .          .
 502   ]
 503   check-trace-count-for-label 1, [print-character]
 504 ]
 505 
 506 scenario editor-inserts-characters-at-cursor-5 [
 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   assume-console [
 515   ¦ left-click 1, 5  # right of non-last line
 516   ¦ type [e]
 517   ]
 518   run [
 519   ¦ editor-event-loop screen, console, e
 520   ]
 521   screen-should-contain [
 522   ¦ .          .
 523   ¦ .abce      .
 524   ¦ .d         .
 525   ¦ .╌╌╌╌╌╌╌╌╌╌.
 526   ¦ .          .
 527   ]
 528   check-trace-count-for-label 1, [print-character]
 529 ]
 530 
 531 scenario editor-inserts-characters-at-cursor-3 [
 532   local-scope
 533   assume-screen 10/width, 5/height
 534   e:&:editor <- new-editor [abc], 0/left, 10/right
 535   editor-render screen, e
 536   $clear-trace
 537   assume-console [
 538   ¦ left-click 3, 5  # below all text
 539   ¦ type [d]
 540   ]
 541   run [
 542   ¦ editor-event-loop screen, console, e
 543   ]
 544   screen-should-contain [
 545   ¦ .          .
 546   ¦ .abcd      .
 547   ¦ .╌╌╌╌╌╌╌╌╌╌.
 548   ¦ .          .
 549   ]
 550   check-trace-count-for-label 1, [print-character]
 551 ]
 552 
 553 scenario editor-inserts-characters-at-cursor-4 [
 554   local-scope
 555   assume-screen 10/width, 5/height
 556   s:text <- new [abc
 557 d]
 558   e:&:editor <- new-editor s, 0/left, 10/right
 559   editor-render screen, e
 560   $clear-trace
 561   assume-console [
 562   ¦ left-click 3, 5  # below all text
 563   ¦ type [e]
 564   ]
 565   run [
 566   ¦ editor-event-loop screen, console, e
 567   ]
 568   screen-should-contain [
 569   ¦ .          .
 570   ¦ .abc       .
 571   ¦ .de        .
 572   ¦ .╌╌╌╌╌╌╌╌╌╌.
 573   ¦ .          .
 574   ]
 575   check-trace-count-for-label 1, [print-character]
 576 ]
 577 
 578 scenario editor-inserts-characters-at-cursor-6 [
 579   local-scope
 580   assume-screen 10/width, 5/height
 581   s:text <- new [abc
 582 d]
 583   e:&:editor <- new-editor s, 0/left, 10/right
 584   editor-render screen, e
 585   $clear-trace
 586   assume-console [
 587   ¦ left-click 3, 5  # below all text
 588   ¦ type [ef]
 589   ]
 590   run [
 591   ¦ editor-event-loop screen, console, e
 592   ]
 593   screen-should-contain [
 594   ¦ .          .
 595   ¦ .abc       .
 596   ¦ .def       .
 597   ¦ .╌╌╌╌╌╌╌╌╌╌.
 598   ¦ .          .
 599   ]
 600   check-trace-count-for-label 2, [print-character]
 601 ]
 602 
 603 scenario editor-moves-cursor-after-inserting-characters [
 604   local-scope
 605   assume-screen 10/width, 5/height
 606   e:&:editor <- new-editor [ab], 0/left, 5/right
 607   editor-render screen, e
 608   assume-console [
 609   ¦ type [01]
 610   ]
 611   run [
 612   ¦ editor-event-loop screen, console, e
 613   ]
 614   screen-should-contain [
 615   ¦ .          .
 616   ¦ .01ab      .
 617   ¦ .╌╌╌╌╌     .
 618   ¦ .          .
 619   ]
 620 ]
 621 
 622 # if the cursor reaches the right margin, wrap the line
 623 
 624 scenario editor-wraps-line-on-insert [
 625   local-scope
 626   assume-screen 5/width, 5/height
 627   e:&:editor <- new-editor [abc], 0/left, 5/right
 628   editor-render screen, e
 629   # type a letter
 630   assume-console [
 631   ¦ type [e]
 632   ]
 633   run [
 634   ¦ editor-event-loop screen, console, e
 635   ]
 636   # no wrap yet
 637   screen-should-contain [
 638   ¦ .     .
 639   ¦ .eabc .
 640   ¦ .╌╌╌╌╌.
 641   ¦ .     .
 642   ¦ .     .
 643   ]
 644   # type a second letter
 645   assume-console [
 646   ¦ type [f]
 647   ]
 648   run [
 649   ¦ editor-event-loop screen, console, e
 650   ]
 651   # now wrap
 652   screen-should-contain [
 653   ¦ .     .
 654   ¦ .efab↩.
 655   ¦ .c    .
 656   ¦ .╌╌╌╌╌.
 657   ¦ .     .
 658   ]
 659 ]
 660 
 661 scenario editor-wraps-line-on-insert-2 [
 662   local-scope
 663   # create an editor with some text
 664   assume-screen 10/width, 5/height
 665   s:text <- new [abcdefg
 666 defg]
 667   e:&:editor <- new-editor s, 0/left, 5/right
 668   editor-render screen, e
 669   # type more text at the start
 670   assume-console [
 671   ¦ left-click 3, 0
 672   ¦ type [abc]
 673   ]
 674   run [
 675   ¦ editor-event-loop screen, console, e
 676   ¦ 3:num/raw <- get *e, cursor-row:offset
 677   ¦ 4:num/raw <- get *e, cursor-column:offset
 678   ]
 679   # cursor is not wrapped
 680   memory-should-contain [
 681   ¦ 3 <- 3
 682   ¦ 4 <- 3
 683   ]
 684   # but line is wrapped
 685   screen-should-contain [
 686   ¦ .          .
 687   ¦ .abcd↩     .
 688   ¦ .efg       .
 689   ¦ .abcd↩     .
 690   ¦ .efg       .
 691   ]
 692 ]
 693 
 694 after <insert-character-special-case> [
 695   # if the line wraps at the cursor, move cursor to start of next row
 696   {
 697   ¦ # if either:
 698   ¦ # a) we're at the end of the line and at the column of the wrap indicator, or
 699   ¦ # b) we're not at end of line and just before the column of the wrap indicator
 700   ¦ wrap-column:num <- copy right
 701   ¦ before-wrap-column:num <- subtract wrap-column, 1
 702   ¦ at-wrap?:bool <- greater-or-equal cursor-column, wrap-column
 703   ¦ just-before-wrap?:bool <- greater-or-equal cursor-column, before-wrap-column
 704   ¦ next:&:duplex-list:char <- next before-cursor
 705   ¦ # at end of line? next == 0 || next.value == 10/newline
 706   ¦ at-end-of-line?:bool <- equal next, 0
 707   ¦ {
 708   ¦ ¦ break-if at-end-of-line?
 709   ¦ ¦ next-character:char <- get *next, value:offset
 710   ¦ ¦ at-end-of-line? <- equal next-character, 10/newline
 711   ¦ }
 712   ¦ # break unless ((eol? and at-wrap?) or (~eol? and just-before-wrap?))
 713   ¦ move-cursor-to-next-line?:bool <- copy 0/false
 714   ¦ {
 715   ¦ ¦ break-if at-end-of-line?
 716   ¦ ¦ move-cursor-to-next-line? <- copy just-before-wrap?
 717   ¦ ¦ # if we're moving the cursor because it's in the middle of a wrapping
 718   ¦ ¦ # line, adjust it to left-most column
 719   ¦ ¦ potential-new-cursor-column:num <- copy left
 720   ¦ }
 721   ¦ {
 722   ¦ ¦ break-unless at-end-of-line?
 723   ¦ ¦ move-cursor-to-next-line? <- copy at-wrap?
 724   ¦ ¦ # if we're moving the cursor because it's at the end of a wrapping line,
 725   ¦ ¦ # adjust it to one past the left-most column to make room for the
 726   ¦ ¦ # newly-inserted wrap-indicator
 727   ¦ ¦ potential-new-cursor-column:num <- add left, 1/make-room-for-wrap-indicator
 728   ¦ }
 729   ¦ break-unless move-cursor-to-next-line?
 730   ¦ cursor-column <- copy potential-new-cursor-column
 731   ¦ *editor <- put *editor, cursor-column:offset, cursor-column
 732   ¦ cursor-row <- add cursor-row, 1
 733   ¦ *editor <- put *editor, cursor-row:offset, cursor-row
 734   ¦ # if we're out of the screen, scroll down
 735   ¦ {
 736   ¦ ¦ below-screen?:bool <- greater-or-equal cursor-row, screen-height
 737   ¦ ¦ break-unless below-screen?
 738   ¦ ¦ <scroll-down>
 739   ¦ }
 740   ¦ return 1/go-render
 741   }
 742 ]
 743 
 744 scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [
 745   local-scope
 746   assume-screen 10/width, 5/height
 747   e:&:editor <- new-editor [abcde], 0/left, 5/right
 748   assume-console [
 749   ¦ left-click 1, 3  # right before the wrap icon
 750   ¦ type [f]
 751   ]
 752   run [
 753   ¦ editor-event-loop screen, console, e
 754   ¦ 3:num/raw <- get *e, cursor-row:offset
 755   ¦ 4:num/raw <- get *e, cursor-column:offset
 756   ]
 757   screen-should-contain [
 758   ¦ .          .
 759   ¦ .abcf↩     .
 760   ¦ .de        .
 761   ¦ .╌╌╌╌╌     .
 762   ¦ .          .
 763   ]
 764   memory-should-contain [
 765   ¦ 3 <- 2  # cursor row
 766   ¦ 4 <- 0  # cursor column
 767   ]
 768 ]
 769 
 770 scenario editor-wraps-cursor-after-inserting-characters-at-end-of-line [
 771   local-scope
 772   assume-screen 10/width, 5/height
 773   # create an editor containing two lines
 774   s:text <- new [abc
 775 xyz]
 776   e:&:editor <- new-editor s, 0/left, 5/right
 777   editor-render screen, e
 778   screen-should-contain [
 779   ¦ .          .
 780   ¦ .abc       .
 781   ¦ .xyz       .
 782   ¦ .╌╌╌╌╌     .
 783   ¦ .          .
 784   ]
 785   assume-console [
 786   ¦ left-click 1, 4  # at end of first line
 787   ¦ type [de]  # trigger wrap
 788   ]
 789   run [
 790   ¦ editor-event-loop screen, console, e
 791   ]
 792   screen-should-contain [
 793   ¦ .          .
 794   ¦ .abcd↩     .
 795   ¦ .e         .
 796   ¦ .xyz       .
 797   ¦ .╌╌╌╌╌     .
 798   ]
 799 ]
 800 
 801 scenario editor-wraps-cursor-to-left-margin [
 802   local-scope
 803   assume-screen 10/width, 5/height
 804   e:&:editor <- new-editor [abcde], 2/left, 7/right
 805   assume-console [
 806   ¦ left-click 1, 5  # line is full; no wrap icon yet
 807   ¦ type [01]
 808   ]
 809   run [
 810   ¦ editor-event-loop screen, console, e
 811   ¦ 3:num/raw <- get *e, cursor-row:offset
 812   ¦ 4:num/raw <- get *e, cursor-column:offset
 813   ]
 814   screen-should-contain [
 815   ¦ .          .
 816   ¦ .  abc0↩   .
 817   ¦ .  1de     .
 818   ¦ .  ╌╌╌╌╌   .
 819   ¦ .          .
 820   ]
 821   memory-should-contain [
 822   ¦ 3 <- 2  # cursor row
 823   ¦ 4 <- 3  # cursor column
 824   ]
 825 ]
 826 
 827 # if newline, move cursor to start of next line, and maybe align indent with previous line
 828 
 829 container editor [
 830   indent?:bool
 831 ]
 832 
 833 after <editor-initialization> [
 834   *result <- put *result, indent?:offset, 1/true
 835 ]
 836 
 837 scenario editor-moves-cursor-down-after-inserting-newline [
 838   local-scope
 839   assume-screen 10/width, 5/height
 840   e:&:editor <- new-editor [abc], 0/left, 10/right
 841   assume-console [
 842   ¦ type [0
 843 1]
 844   ]
 845   run [
 846   ¦ editor-event-loop screen, console, e
 847   ]
 848   screen-should-contain [
 849   ¦ .          .
 850   ¦ .0         .
 851   ¦ .1abc      .
 852   ¦ .╌╌╌╌╌╌╌╌╌╌.
 853   ¦ .          .
 854   ]
 855 ]
 856 
 857 after <handle-special-character> [
 858   {
 859   ¦ newline?:bool <- equal c, 10/newline
 860   ¦ break-unless newline?
 861   ¦ <begin-insert-enter>
 862   ¦ insert-new-line-and-indent editor, screen
 863   ¦ <end-insert-enter>
 864   ¦ return 1/go-render
 865   }
 866 ]
 867 
 868 def insert-new-line-and-indent editor:&:editor, screen:&:screen -> editor:&:editor, screen:&:screen [
 869   local-scope
 870   load-inputs
 871   cursor-row:num <- get *editor, cursor-row:offset
 872   cursor-column:num <- get *editor, cursor-column:offset
 873   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 874   left:num <- get *editor, left:offset
 875   right:num <- get *editor, right:offset
 876   screen-height:num <- screen-height screen
 877   # update cursor coordinates
 878   at-start-of-wrapped-line?:bool <- at-start-of-wrapped-line? editor
 879   {
 880   ¦ break-if at-start-of-wrapped-line?
 881   ¦ cursor-row <- add cursor-row, 1
 882   ¦ *editor <- put *editor, cursor-row:offset, cursor-row
 883   }
 884   cursor-column <- copy left
 885   *editor <- put *editor, cursor-column:offset, cursor-column
 886   # maybe scroll
 887   {
 888   ¦ below-screen?:bool <- greater-or-equal cursor-row, screen-height  # must be equal, never greater
 889   ¦ break-unless below-screen?
 890   ¦ <scroll-down>
 891   ¦ cursor-row <- subtract cursor-row, 1  # bring back into screen range
 892   ¦ *editor <- put *editor, cursor-row:offset, cursor-row
 893   }
 894   # insert newline
 895   insert 10/newline, before-cursor
 896   before-cursor <- next before-cursor
 897   *editor <- put *editor, before-cursor:offset, before-cursor
 898   # indent if necessary
 899   indent?:bool <- get *editor, indent?:offset
 900   return-unless indent?
 901   d:&:duplex-list:char <- get *editor, data:offset
 902   end-of-previous-line:&:duplex-list:char <- prev before-cursor
 903   indent:num <- line-indent end-of-previous-line, d
 904   i:num <- copy 0
 905   {
 906   ¦ indent-done?:bool <- greater-or-equal i, indent
 907   ¦ break-if indent-done?
 908   ¦ insert-at-cursor editor, 32/space, screen
 909   ¦ i <- add i, 1
 910   ¦ loop
 911   }
 912 ]
 913 
 914 def at-start-of-wrapped-line? editor:&:editor -> result:bool [
 915   local-scope
 916   load-inputs
 917   left:num <- get *editor, left:offset
 918   cursor-column:num <- get *editor, cursor-column:offset
 919   cursor-at-left?:bool <- equal cursor-column, left
 920   return-unless cursor-at-left?, 0/false
 921   before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
 922   before-before-cursor:&:duplex-list:char <- prev before-cursor
 923   return-unless before-before-cursor, 0/false  # cursor is at start of editor
 924   char-before-cursor:char <- get *before-cursor, value:offset
 925   cursor-after-newline?:bool <- equal char-before-cursor, 10/newline
 926   return-if cursor-after-newline?, 0/false
 927   # if cursor is at left margin and not at start, but previous character is not a newline,
 928   # then we're at start of a wrapped line
 929   return 1/true
 930 ]
 931 
 932 # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
 933 # the number of spaces at the start of the line containing 'curr'.
 934 def line-indent curr:&:duplex-list:char, start:&:duplex-list:char -> result:num [
 935   local-scope
 936   load-inputs
 937   result:num <- copy 0
 938   return-unless curr
 939   at-start?:bool <- equal curr, start
 940   return-if at-start?
 941   {
 942   ¦ curr <- prev curr
 943   ¦ break-unless curr
 944   ¦ at-start?:bool <- equal curr, start
 945   ¦ break-if at-start?
 946   ¦ c:char <- get *curr, value:offset
 947   ¦ at-newline?:bool <- equal c, 10/newline
 948   ¦ break-if at-newline?
 949   ¦ # if c is a space, increment result
 950   ¦ is-space?:bool <- equal c, 32/space
 951   ¦ {
 952   ¦ ¦ break-unless is-space?
 953   ¦ ¦ result <- add result, 1
 954   ¦ }
 955   ¦ # if c is not a space, reset result
 956   ¦ {
 957   ¦ ¦ break-if is-space?
 958   ¦ ¦ result <- copy 0
 959   ¦ }
 960   ¦ loop
 961   }
 962 ]
 963 
 964 scenario editor-moves-cursor-down-after-inserting-newline-2 [
 965   local-scope
 966   assume-screen 10/width, 5/height
 967   e:&:editor <- new-editor [abc], 1/left, 10/right
 968   assume-console [
 969   ¦ type [0
 970 1]
 971   ]
 972   run [
 973   ¦ editor-event-loop screen, console, e
 974   ]
 975   screen-should-contain [
 976   ¦ .          .
 977   ¦ . 0        .
 978   ¦ . 1abc     .
 979   ¦ . ╌╌╌╌╌╌╌╌╌.
 980   ¦ .          .
 981   ]
 982 ]
 983 
 984 scenario editor-clears-previous-line-completely-after-inserting-newline [
 985   local-scope
 986   assume-screen 10/width, 5/height
 987   e:&:editor <- new-editor [abcde], 0/left, 5/right
 988   editor-render screen, e
 989   screen-should-contain [
 990   ¦ .          .
 991   ¦ .abcd↩     .
 992   ¦ .e         .
 993   ¦ .╌╌╌╌╌     .
 994   ¦ .          .
 995   ]
 996   assume-console [
 997   ¦ press enter
 998   ]
 999   run [
1000   ¦ editor-event-loop screen, console, e
1001   ]
1002   # line should be fully cleared
1003   screen-should-contain [
1004   ¦ .          .
1005   ¦ .          .
1006   ¦ .abcd↩     .
1007   ¦ .e         .
1008   ¦ .╌╌╌╌╌     .
1009   ]
1010 ]
1011 
1012 scenario editor-splits-wrapped-line-after-inserting-newline [
1013   local-scope
1014   assume-screen 10/width, 5/height
1015   e:&:editor <- new-editor [abcdef], 0/left, 5/right
1016   editor-render screen, e
1017   screen-should-contain [
1018   ¦ .          .
1019   ¦ .abcd↩     .
1020   ¦ .ef        .
1021   ¦ .╌╌╌╌╌     .
1022   ¦ .          .
1023   ]
1024   assume-console [
1025   ¦ left-click 2, 0
1026   ¦ press enter
1027   ]
1028   run [
1029   ¦ editor-event-loop screen, console, e
1030   ¦ 10:num/raw <- get *e, cursor-row:offset
1031   ¦ 11:num/raw <- get *e, cursor-column:offset
1032   ]
1033   screen-should-contain [
1034   ¦ .          .
1035   ¦ .abcd      .
1036   ¦ .ef        .
1037   ¦ .╌╌╌╌╌     .
1038   ]
1039   memory-should-contain [
1040   ¦ 10 <- 2  # cursor-row
1041   ¦ 11 <- 0  # cursor-column
1042   ]
1043 ]
1044 
1045 scenario editor-inserts-indent-after-newline [
1046   local-scope
1047   assume-screen 10/width, 10/height
1048   s:text <- new [ab
1049   cd
1050 ef]
1051   e:&:editor <- new-editor s, 0/left, 10/right
1052   # position cursor after 'cd' and hit 'newline'
1053   assume-console [
1054   ¦ left-click 2, 8
1055   ¦ type [
1056 ]
1057   ]
1058   run [
1059   ¦ editor-event-loop screen, console, e
1060   ¦ 3:num/raw <- get *e, cursor-row:offset
1061   ¦ 4:num/raw <- get *e, cursor-column:offset
1062   ]
1063   # cursor should be below start of previous line
1064   memory-should-contain [
1065   ¦ 3 <- 3  # cursor row
1066   ¦ 4 <- 2  # cursor column (indented)
1067   ]
1068 ]
1069 
1070 scenario editor-skips-indent-around-paste [
1071   local-scope
1072   assume-screen 10/width, 10/height
1073   s:text <- new [ab
1074   cd
1075 ef]
1076   e:&:editor <- new-editor s, 0/left, 10/right
1077   # position cursor after 'cd' and hit 'newline' surrounded by paste markers
1078   assume-console [
1079   ¦ left-click 2, 8
1080   ¦ press 65507  # start paste
1081   ¦ press enter
1082   ¦ press 65506  # end paste
1083   ]
1084   run [
1085   ¦ editor-event-loop screen, console, e
1086   ¦ 3:num/raw <- get *e, cursor-row:offset
1087   ¦ 4:num/raw <- get *e, cursor-column:offset
1088   ]
1089   # cursor should be below start of previous line
1090   memory-should-contain [
1091   ¦ 3 <- 3  # cursor row
1092   ¦ 4 <- 0  # cursor column (not indented)
1093   ]
1094 ]
1095 
1096 after <handle-special-key> [
1097   {
1098   ¦ paste-start?:bool <- equal k, 65507/paste-start
1099   ¦ break-unless paste-start?
1100   ¦ *editor <- put *editor, indent?:offset, 0/false
1101   ¦ return 1/go-render
1102   }
1103 ]
1104 
1105 after <handle-special-key> [
1106   {
1107   ¦ paste-end?:bool <- equal k, 65506/paste-end
1108   ¦ break-unless paste-end?
1109   ¦ *editor <- put *editor, indent?:offset, 1/true
1110   ¦ return 1/go-render
1111   }
1112 ]
1113 
1114 ## helpers
1115 
1116 def draw-horizontal screen:&:screen, row:num, x:num, right:num -> screen:&:screen [
1117   local-scope
1118   load-inputs
1119   height:num <- screen-height screen
1120   past-bottom?:bool <- greater-or-equal row, height
1121   return-if past-bottom?
1122   style:char, style-found?:bool <- next-input
1123   {
1124   ¦ break-if style-found?
1125   ¦ style <- copy 9472/horizontal
1126   }
1127   color:num, color-found?:bool <- next-input
1128   {
1129   ¦ # default color to white
1130   ¦ break-if color-found?
1131   ¦ color <- copy 245/grey
1132   }
1133   bg-color:num, bg-color-found?:bool <- next-input
1134   {
1135   ¦ break-if bg-color-found?
1136   ¦ bg-color <- copy 0/black
1137   }
1138   screen <- move-cursor screen, row, x
1139   {
1140   ¦ continue?:bool <- lesser-or-equal x, right  # right is inclusive, to match editor semantics
1141   ¦ break-unless continue?
1142   ¦ print screen, style, color, bg-color
1143   ¦ x <- add x, 1
1144   ¦ loop
1145   }
1146 ]