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