about summary refs log tree commit diff stats
path: root/edit/002-typing.mu
blob: f3f6b32127c2c27e33bc69f123be21a85b2708da (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
Ranger v.1.0.4
==============

Ranger

   A keeper, guardian, or soldier who ranges over a region
   to protect the area or enforce the law.

This is the filemanager Chuck Norris the Texas Ranger would use
if he had a computer with a unix-like operating system.  (He doesn't
trust computers though and prefers to do the calculations himself.)

After all, as a professional ranger, he needs a broad overview of his
territory, and the multi-column display of ranger provides just that,
rather than restricting you to the current directory only.
You can preview the content of the selected file or directory, copy or move
files around with the VIM-like commands dd and yy, execute predefined
applications when opening a file, etc...

Everything is fully customizable and written in Python (2.6 and 3.1
compatible) using curses for the text-based user interface.


About
-----

* Author:          Roman Zimbelmann
* Email:           romanz@lavabit.com
* Website:         http://savannah.nongnu.org/projects/ranger
* Git repo:        http://git.savannah.gnu.org/cgit/ranger.git
* Version:         1.0.4


Features
--------

* Multi-column display
* Preview of the selected file/directory
* Common file operations (create/chmod/copy/delete/...)
* Quickly find files or text inside files
* VIM-like console and hotkeys
* Open files in external programs
* Mouse support
* Change the directory of your shell after exiting ranger
* Bookmarks


Dependencies
------------

* A Unix-like Operating System
* Python 2.6 or 3.1
* Python curses module  (often but not always included with Python)


Bugs and Feature Requests
-------------------------

Report bugs and feature requests on savannah:
    https://savannah.nongnu.org/bugs/?func=additem&group=ranger

Alternatively you can send an email to romanz@lavabit.com.

Please include as much relevant information as possible.
Using ranger with the --debug option will abort the program and
print tracebacks rather than a red message in the statusbar.
If this applies to you, please include such a traceback in your report.


Getting Started
---------------

If you just want to check out ranger without installing it, type

    ./ranger.py --clean

in the top directory of ranger.  By using the --clean switch, it will
leave no trace on your system whatsoever.

To properly install it, follow the instructions in the INSTALL file,
then type:

    ranger

You should see 4 columns.  The third is the directory where you are at
the moment.  To the left, there are the directories above the current
working directory, and the column on the right is a preview of the selected
file/directory.

Now use the arrow keys to navigate, press enter to open a file.

A list of commands with short descriptions can be viewed by
pressing "?" inside the program and following the instructions.
The file ranger/defaults/keys.py contains all key combinations, so that's
another place you may want to check out.


Opening Files with Ranger
-------------------------

If you use the same applications like me, you'll be able to open
files by pressing the right arrow key.  If not, you will have to
specify them in ranger/defaults/apps.py.  It's explained
in the docstrings how exactly to do that.

Once you've set up your applications, you can also use ranger to
open files from the shell:
    ranger blabla.pdf


Customizing Ranger
------------------

The file ranger/defaults/options.py contains most of the options.
apps.py defines how files are run, keys.py defines keybindings.

The files in ranger/defaults/ can be copied into ~/.ranger/ for per-user
modifications.  Colorschemes can be placed in ~/.ranger/colorschemes.

The configuration files should be self-explanatory.  If you need more
information, check out the source code.

Also, see the file HACKING for more detailed instructions on
modifying the program.


Tips
----

Change the directory of your parent shell when you exit ranger:

ranger() {
    $(which ranger) $@ &&
    cd "$(grep \^\' ~/.ranger/bookmarks | cut -b3-)"
}

This can be put into your ~/.bashrc or something similar.
15'>415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042
## handling events from the keyboard, mouse, touch screen, ...

# temporary main: interactive editor
# hit ctrl-c to exit
recipe! main text:address:shared:array:character [
  local-scope
  load-ingredients
  open-console
  editor:address:shared:editor-data <- new-editor text, 0/screen, 5/left, 45/right
  editor-event-loop 0/screen, 0/console, editor
  close-console
]

recipe editor-event-loop screen:address:shared:screen, console:address:shared:console, editor:address:shared:editor-data -> screen:address:shared:screen, console:address:shared:console, editor:address:shared:editor-data [
  local-scope
  load-ingredients
  {
    # looping over each (keyboard or touch) event as it occurs
    +next-event
    cursor-row:number <- get *editor, cursor-row:offset
    cursor-column:number <- get *editor, cursor-column:offset
    screen <- move-cursor screen, cursor-row, cursor-column
    e:event, console:address:shared:console, found?:boolean, quit?:boolean <- read-event console
    loop-unless found?
    break-if quit?  # only in tests
    trace 10, [app], [next-event]
    # 'touch' event
    t:address:touch-event <- maybe-convert e, touch:variant
    {
      break-unless t
      move-cursor-in-editor screen, editor, *t
      loop +next-event:label
    }
    # keyboard events
    {
      break-if t
      screen, editor, go-render?:boolean <- handle-keyboard-event screen, editor, e
      {
        break-unless go-render?
        screen <- editor-render screen, editor
      }
    }
    loop
  }
]

# process click, return if it was on current editor
recipe move-cursor-in-editor screen:address:shared:screen, editor:address:shared:editor-data, t:touch-event -> in-focus?:boolean, editor:address:shared:editor-data [
  local-scope
  load-ingredients
  reply-unless editor, 0/false
  click-row:number <- get t, row:offset
  reply-unless click-row, 0/false  # ignore clicks on 'menu'
  click-column:number <- get t, column:offset
  left:number <- get *editor, left:offset
  too-far-left?:boolean <- lesser-than click-column, left
  reply-if too-far-left?, 0/false
  right:number <- get *editor, right:offset
  too-far-right?:boolean <- greater-than click-column, right
  reply-if too-far-right?, 0/false
  # position cursor
  <move-cursor-begin>
  editor <- snap-cursor screen, editor, click-row, click-column
  undo-coalesce-tag:number <- copy 0/never
  <move-cursor-end>
  # gain focus
  reply 1/true
]

# Variant of 'render' that only moves the cursor (coordinates and
# before-cursor). If it's past the end of a line, it 'slides' it left. If it's
# past the last line it positions at end of last line.
recipe snap-cursor screen:address:shared:screen, editor:address:shared:editor-data, target-row:number, target-column:number -> editor:address:shared:editor-data [
  local-scope
  load-ingredients
  reply-unless editor
  left:number <- get *editor, left:offset
  right:number <- get *editor, right:offset
  screen-height:number <- screen-height screen
  # count newlines until screen row
  curr:address:shared:duplex-list:character <- get *editor, top-of-screen:offset
  prev:address:shared:duplex-list:character <- copy curr  # just in case curr becomes null and we can't compute prev
  curr <- next curr
  row:number <- copy 1/top
  column:number <- copy left
  cursor-row:address:number <- get-address *editor, cursor-row:offset
  *cursor-row <- copy target-row
  cursor-column:address:number <- get-address *editor, cursor-column:offset
  *cursor-column <- copy target-column
  before-cursor:address:address:shared:duplex-list:character <- get-address *editor, before-cursor:offset
  {
    +next-character
    break-unless curr
    off-screen?:boolean <- greater-or-equal row, screen-height
    break-if off-screen?
    # update editor-data.before-cursor
    # Doing so at the start of each iteration ensures it stays one step behind
    # the current character.
    {
      at-cursor-row?:boolean <- equal row, *cursor-row
      break-unless at-cursor-row?
      at-cursor?:boolean <- equal column, *cursor-column
      break-unless at-cursor?
      *before-cursor <- copy prev
    }
    c:character <- get *curr, value:offset
    {
      # newline? move to left rather than 0
      newline?:boolean <- equal c, 10/newline
      break-unless newline?
      # adjust cursor if necessary
      {
        at-cursor-row?:boolean <- equal row, *cursor-row
        break-unless at-cursor-row?
        left-of-cursor?:boolean <- lesser-than column, *cursor-column
        break-unless left-of-cursor?
        *cursor-column <- copy column
        *before-cursor <- copy prev
      }
      # skip to next line
      row <- add row, 1
      column <- copy left
      curr <- next curr
      prev <- next prev
      loop +next-character:label
    }
    {
      # at right? wrap. even if there's only one more letter left; we need
      # room for clicking on the cursor after it.
      at-right?:boolean <- equal column, right
      break-unless at-right?
      column <- copy left
      row <- add row, 1
      # don't increment curr/prev
      loop +next-character:label
    }
    curr <- next curr
    prev <- next prev
    column <- add column, 1
    loop
  }
  # is cursor to the right of the last line? move to end
  {
    at-cursor-row?:boolean <- equal row, *cursor-row
    cursor-outside-line?:boolean <- lesser-or-equal column, *cursor-column
    before-cursor-on-same-line?:boolean <- and at-cursor-row?, cursor-outside-line?
    above-cursor-row?:boolean <- lesser-than row, *cursor-row
    before-cursor?:boolean <- or before-cursor-on-same-line?, above-cursor-row?
    break-unless before-cursor?
    *cursor-row <- copy row
    *cursor-column <- copy column
    *before-cursor <- copy prev
  }
]

# Process an event 'e' and try to minimally update the screen.
# Set 'go-render?' to true to indicate the caller must perform a non-minimal update.
recipe handle-keyboard-event screen:address:shared:screen, editor:address:shared:editor-data, e:event -> screen:address:shared:screen, editor:address:shared:editor-data, go-render?:boolean [
  local-scope
  load-ingredients
  go-render? <- copy 0/false
  reply-unless editor
  screen-width:number <- screen-width screen
  screen-height:number <- screen-height screen
  left:number <- get *editor, left:offset
  right:number <- get *editor, right:offset
  before-cursor:address:address:shared:duplex-list:character <- get-address *editor, before-cursor:offset
  cursor-row:address:number <- get-address *editor, cursor-row:offset
  cursor-column:address:number <- get-address *editor, cursor-column:offset
  save-row:number <- copy *cursor-row
  save-column:number <- copy *cursor-column
  # character
  {
    c:address:character <- maybe-convert e, text:variant
    break-unless c
    trace 10, [app], [handle-keyboard-event: special character]
    # exceptions for special characters go here
    <handle-special-character>
    # ignore any other special characters
    regular-character?:boolean <- greater-or-equal *c, 32/space
    go-render? <- copy 0/false
    reply-unless regular-character?
    # otherwise type it in
    <insert-character-begin>
    editor, screen, go-render?:boolean <- insert-at-cursor editor, *c, screen
    <insert-character-end>
    reply
  }
  # special key to modify the text or move the cursor
  k:address:number <- maybe-convert e:event, keycode:variant
  assert k, [event was of unknown type; neither keyboard nor mouse]
  # handlers for each special key will go here
  <handle-special-key>
  go-render? <- copy 1/true
  reply
]

recipe insert-at-cursor editor:address:shared:editor-data, c:character, screen:address:shared:screen -> editor:address:shared:editor-data, screen:address:shared:screen, go-render?:boolean [
  local-scope
  load-ingredients
  before-cursor:address:address:shared:duplex-list:character <- get-address *editor, before-cursor:offset
  insert c, *before-cursor
  *before-cursor <- next *before-cursor
  cursor-row:address:number <- get-address *editor, cursor-row:offset
  cursor-column:address:number <- get-address *editor, cursor-column:offset
  left:number <- get *editor, left:offset
  right:number <- get *editor, right:offset
  save-row:number <- copy *cursor-row
  save-column:number <- copy *cursor-column
  screen-width:number <- screen-width screen
  screen-height:number <- screen-height screen
  # occasionally we'll need to mess with the cursor
  <insert-character-special-case>
  # but mostly we'll just move the cursor right
  *cursor-column <- add *cursor-column, 1
  next:address:shared:duplex-list:character <- next *before-cursor
  {
    # at end of all text? no need to scroll? just print the character and leave
    at-end?:boolean <- equal next, 0/null
    break-unless at-end?
    bottom:number <- subtract screen-height, 1
    at-bottom?:boolean <- equal save-row, bottom
    at-right?:boolean <- equal save-column, right
    overflow?:boolean <- and at-bottom?, at-right?
    break-if overflow?
    move-cursor screen, save-row, save-column
    print screen, c
    go-render? <- copy 0/false
    reply
  }
  {
    # not at right margin? print the character and rest of line
    break-unless next
    at-right?:boolean <- greater-or-equal *cursor-column, screen-width
    break-if at-right?
    curr:address:shared:duplex-list:character <- copy *before-cursor
    move-cursor screen, save-row, save-column
    curr-column:number <- copy save-column
    {
      # hit right margin? give up and let caller render
      go-render? <- copy 1/true
      at-right?:boolean <- greater-than curr-column, right
      reply-if at-right?
      break-unless curr
      # newline? done.
      currc:character <- get *curr, value:offset
      at-newline?:boolean <- equal currc, 10/newline
      break-if at-newline?
      print screen, currc
      curr-column <- add curr-column, 1
      curr <- next curr
      loop
    }
    go-render? <- copy 0/false
    reply
  }
  go-render? <- copy 1/true
  reply
]

# helper for tests
recipe editor-render screen:address:shared:screen, editor:address:shared:editor-data -> screen:address:shared:screen, editor:address:shared:editor-data [
  local-scope
  load-ingredients
  left:number <- get *editor, left:offset
  right:number <- get *editor, right:offset
  row:number, column:number <- render screen, editor
  clear-line-delimited screen, column, right
  row <- add row, 1
  draw-horizontal screen, row, left, right, 9480/horizontal-dotted
  row <- add row, 1
  clear-screen-from screen, row, left, left, right
]

scenario editor-handles-empty-event-queue [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  assume-console []
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .abc       .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
]

scenario editor-handles-mouse-clicks [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    left-click 1, 1  # on the 'b'
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  screen-should-contain [
    .          .
    .abc       .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
  memory-should-contain [
    3 <- 1  # cursor is at row 0..
    4 <- 1  # ..and column 1
  ]
  check-trace-count-for-label 0, [print-character]
]

scenario editor-handles-mouse-clicks-outside-text [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  $clear-trace
  assume-console [
    left-click 1, 7  # last line, to the right of text
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 1  # cursor row
    4 <- 3  # cursor column
  ]
  check-trace-count-for-label 0, [print-character]
]

scenario editor-handles-mouse-clicks-outside-text-2 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc
def]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  $clear-trace
  assume-console [
    left-click 1, 7  # interior line, to the right of text
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 1  # cursor row
    4 <- 3  # cursor column
  ]
  check-trace-count-for-label 0, [print-character]
]

scenario editor-handles-mouse-clicks-outside-text-3 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc
def]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  $clear-trace
  assume-console [
    left-click 3, 7  # below text
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  memory-should-contain [
    3 <- 2  # cursor row
    4 <- 3  # cursor column
  ]
  check-trace-count-for-label 0, [print-character]
]

scenario editor-handles-mouse-clicks-outside-column [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  # editor occupies only left half of screen
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    # click on right half of screen
    left-click 3, 8
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  screen-should-contain [
    .          .
    .abc       .
    .┈┈┈┈┈     .
    .          .
  ]
  memory-should-contain [
    3 <- 1  # no change to cursor row
    4 <- 0  # ..or column
  ]
  check-trace-count-for-label 0, [print-character]
]

scenario editor-handles-mouse-clicks-in-menu-area [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    # click on first, 'menu' row
    left-click 0, 3
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  # no change to cursor
  memory-should-contain [
    3 <- 1
    4 <- 0
  ]
]

scenario editor-inserts-characters-into-empty-editor [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new []
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    type [abc]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .abc       .
    .┈┈┈┈┈     .
    .          .
  ]
  check-trace-count-for-label 3, [print-character]
]

scenario editor-inserts-characters-at-cursor [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  # type two letters at different places
  assume-console [
    type [0]
    left-click 1, 2
    type [d]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .0adbc     .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
  check-trace-count-for-label 7, [print-character]  # 4 for first letter, 3 for second
]

scenario editor-inserts-characters-at-cursor-2 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    left-click 1, 5  # right of last line
    type [d]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .abcd      .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
  check-trace-count-for-label 1, [print-character]
]

scenario editor-inserts-characters-at-cursor-5 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc
d]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    left-click 1, 5  # right of non-last line
    type [e]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .abce      .
    .d         .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
  check-trace-count-for-label 1, [print-character]
]

scenario editor-inserts-characters-at-cursor-3 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    left-click 3, 5  # below all text
    type [d]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .abcd      .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
  check-trace-count-for-label 1, [print-character]
]

scenario editor-inserts-characters-at-cursor-4 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc
d]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    left-click 3, 5  # below all text
    type [e]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .abc       .
    .de        .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
  check-trace-count-for-label 1, [print-character]
]

scenario editor-inserts-characters-at-cursor-6 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc
d]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  editor-render screen, 2:address:shared:editor-data
  $clear-trace
  assume-console [
    left-click 3, 5  # below all text
    type [ef]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .abc       .
    .def       .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
  check-trace-count-for-label 2, [print-character]
]

scenario editor-moves-cursor-after-inserting-characters [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [ab]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  editor-render screen, 2:address:shared:editor-data
  assume-console [
    type [01]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .01ab      .
    .┈┈┈┈┈     .
    .          .
  ]
]

# if the cursor reaches the right margin, wrap the line

scenario editor-wraps-line-on-insert [
  assume-screen 5/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  editor-render screen, 2:address:shared:editor-data
  # type a letter
  assume-console [
    type [e]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  # no wrap yet
  screen-should-contain [
    .     .
    .eabc .
    .┈┈┈┈┈.
    .     .
    .     .
  ]
  # type a second letter
  assume-console [
    type [f]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  # now wrap
  screen-should-contain [
    .     .
    .efab.
    .c    .
    .┈┈┈┈┈.
    .     .
  ]
]

scenario editor-wraps-line-on-insert-2 [
  # create an editor with some text
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abcdefg
defg]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  editor-render screen, 2:address:shared:editor-data
  # type more text at the start
  assume-console [
    left-click 3, 0
    type [abc]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  # cursor is not wrapped
  memory-should-contain [
    3 <- 3
    4 <- 3
  ]
  # but line is wrapped
  screen-should-contain [
    .          .
    .abcd     .
    .efg       .
    .abcd     .
    .efg       .
  ]
]

after <insert-character-special-case> [
  # if the line wraps at the cursor, move cursor to start of next row
  {
    # if we're at the column just before the wrap indicator
    wrap-column:number <- subtract right, 1
    at-wrap?:boolean <- greater-or-equal *cursor-column, wrap-column
    break-unless at-wrap?
    *cursor-column <- subtract *cursor-column, wrap-column
    *cursor-column <- add *cursor-column, left
    *cursor-row <- add *cursor-row, 1
    # if we're out of the screen, scroll down
    {
      below-screen?:boolean <- greater-or-equal *cursor-row, screen-height
      break-unless below-screen?
      <scroll-down>
    }
    go-render? <- copy 1/true
    reply
  }
]

scenario editor-wraps-cursor-after-inserting-characters [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abcde]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  assume-console [
    left-click 1, 4  # line is full; no wrap icon yet
    type [f]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  screen-should-contain [
    .          .
    .abcd     .
    .fe        .
    .┈┈┈┈┈     .
    .          .
  ]
  memory-should-contain [
    3 <- 2  # cursor row
    4 <- 1  # cursor column
  ]
]

scenario editor-wraps-cursor-after-inserting-characters-2 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abcde]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  assume-console [
    left-click 1, 3  # right before the wrap icon
    type [f]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  screen-should-contain [
    .          .
    .abcf     .
    .de        .
    .┈┈┈┈┈     .
    .          .
  ]
  memory-should-contain [
    3 <- 2  # cursor row
    4 <- 0  # cursor column
  ]
]

scenario editor-wraps-cursor-to-left-margin [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abcde]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 2/left, 7/right
  assume-console [
    left-click 1, 5  # line is full; no wrap icon yet
    type [01]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  screen-should-contain [
    .          .
    .  abc0   .
    .  1de     .
    .  ┈┈┈┈┈   .
    .          .
  ]
  memory-should-contain [
    3 <- 2  # cursor row
    4 <- 3  # cursor column
  ]
]

# if newline, move cursor to start of next line, and maybe align indent with previous line

container editor-data [
  indent?:boolean
]

after <editor-initialization> [
  indent?:address:boolean <- get-address *result, indent?:offset
  *indent? <- copy 1/true
]

scenario editor-moves-cursor-down-after-inserting-newline [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  assume-console [
    type [0
1]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    .0         .
    .1abc      .
    .┈┈┈┈┈┈┈┈┈┈.
    .          .
  ]
]

after <handle-special-character> [
  {
    newline?:boolean <- equal *c, 10/newline
    break-unless newline?
    <insert-enter-begin>
    editor <- insert-new-line-and-indent editor, screen
    <insert-enter-end>
    go-render? <- copy 1/true
    reply
  }
]

recipe insert-new-line-and-indent editor:address:shared:editor-data, screen:address:shared:screen -> editor:address:shared:editor-data, screen:address:shared:screen, go-render?:boolean [
  local-scope
  load-ingredients
  cursor-row:address:number <- get-address *editor, cursor-row:offset
  cursor-column:address:number <- get-address *editor, cursor-column:offset
  before-cursor:address:address:shared:duplex-list:character <- get-address *editor, before-cursor:offset
  left:number <- get *editor, left:offset
  right:number <- get *editor, right:offset
  screen-height:number <- screen-height screen
  # insert newline
  insert 10/newline, *before-cursor
  *before-cursor <- next *before-cursor
  *cursor-row <- add *cursor-row, 1
  *cursor-column <- copy left
  # maybe scroll
  {
    below-screen?:boolean <- greater-or-equal *cursor-row, screen-height  # must be equal, never greater
    break-unless below-screen?
    <scroll-down>
    go-render? <- copy 1/true
    *cursor-row <- subtract *cursor-row, 1  # bring back into screen range
  }
  # indent if necessary
  indent?:boolean <- get *editor, indent?:offset
  reply-unless indent?
  d:address:shared:duplex-list:character <- get *editor, data:offset
  end-of-previous-line:address:shared:duplex-list:character <- prev *before-cursor
  indent:number <- line-indent end-of-previous-line, d
  i:number <- copy 0
  {
    indent-done?:boolean <- greater-or-equal i, indent
    break-if indent-done?
    editor, screen, go-render?:boolean <- insert-at-cursor editor, 32/space, screen
    i <- add i, 1
    loop
  }
]

# takes a pointer 'curr' into the doubly-linked list and its sentinel, counts
# the number of spaces at the start of the line containing 'curr'.
recipe line-indent curr:address:shared:duplex-list:character, start:address:shared:duplex-list:character -> result:number [
  local-scope
  load-ingredients
  result:number <- copy 0
  reply-unless curr
  at-start?:boolean <- equal curr, start
  reply-if at-start?
  {
    curr <- prev curr
    break-unless curr
    at-start?:boolean <- equal curr, start
    break-if at-start?
    c:character <- get *curr, value:offset
    at-newline?:boolean <- equal c, 10/newline
    break-if at-newline?
    # if c is a space, increment result
    is-space?:boolean <- equal c, 32/space
    {
      break-unless is-space?
      result <- add result, 1
    }
    # if c is not a space, reset result
    {
      break-if is-space?
      result <- copy 0
    }
    loop
  }
]

scenario editor-moves-cursor-down-after-inserting-newline-2 [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abc]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 1/left, 10/right
  assume-console [
    type [0
1]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  screen-should-contain [
    .          .
    . 0        .
    . 1abc     .
    . ┈┈┈┈┈┈┈┈┈.
    .          .
  ]
]

scenario editor-clears-previous-line-completely-after-inserting-newline [
  assume-screen 10/width, 5/height
  1:address:shared:array:character <- new [abcde]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  assume-console [
    press enter
  ]
  screen-should-contain [
    .          .
    .abcd     .
    .e         .
    .          .
    .          .
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
  ]
  # line should be fully cleared
  screen-should-contain [
    .          .
    .          .
    .abcd     .
    .e         .
    .┈┈┈┈┈     .
  ]
]

scenario editor-inserts-indent-after-newline [
  assume-screen 10/width, 10/height
  1:address:shared:array:character <- new [ab
  cd
ef]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  # position cursor after 'cd' and hit 'newline'
  assume-console [
    left-click 2, 8
    type [
]
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  # cursor should be below start of previous line
  memory-should-contain [
    3 <- 3  # cursor row
    4 <- 2  # cursor column (indented)
  ]
]

scenario editor-skips-indent-around-paste [
  assume-screen 10/width, 10/height
  1:address:shared:array:character <- new [ab
  cd
ef]
  2:address:shared:editor-data <- new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  # position cursor after 'cd' and hit 'newline' surrounded by paste markers
  assume-console [
    left-click 2, 8
    press 65507  # start paste
    press enter
    press 65506  # end paste
  ]
  run [
    editor-event-loop screen:address:shared:screen, console:address:shared:console, 2:address:shared:editor-data
    3:number <- get *2:address:shared:editor-data, cursor-row:offset
    4:number <- get *2:address:shared:editor-data, cursor-column:offset
  ]
  # cursor should be below start of previous line
  memory-should-contain [
    3 <- 3  # cursor row
    4 <- 0  # cursor column (not indented)
  ]
]

after <handle-special-key> [
  {
    paste-start?:boolean <- equal *k, 65507/paste-start
    break-unless paste-start?
    indent?:address:boolean <- get-address *editor, indent?:offset
    *indent? <- copy 0/false
    go-render? <- copy 1/true
    reply
  }
]

after <handle-special-key> [
  {
    paste-end?:boolean <- equal *k, 65506/paste-end
    break-unless paste-end?
    indent?:address:boolean <- get-address *editor, indent?:offset
    *indent? <- copy 1/true
    go-render? <- copy 1/true
    reply
  }
]

## helpers

recipe draw-horizontal screen:address:shared:screen, row:number, x:number, right:number -> screen:address:shared:screen [
  local-scope
  load-ingredients
  style:character, style-found?:boolean <- next-ingredient
  {
    break-if style-found?
    style <- copy 9472/horizontal
  }
  color:number, color-found?:boolean <- next-ingredient
  {
    # default color to white
    break-if color-found?
    color <- copy 245/grey
  }
  bg-color:number, bg-color-found?:boolean <- next-ingredient
  {
    break-if bg-color-found?
    bg-color <- copy 0/black
  }
  screen <- move-cursor screen, row, x
  {
    continue?:boolean <- lesser-or-equal x, right  # right is inclusive, to match editor-data semantics
    break-unless continue?
    print screen, style, color, bg-color
    x <- add x, 1
    loop
  }
]