about summary refs log tree commit diff stats
path: root/sandbox/001-editor.mu
blob: b3399dbb634c3c06733bf38b1ef8995bcb5b22ff (plain) (blame) pre { 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 */
## the basic editor data structure, and how it displays text to the screen

# temporary main for this layer: just render the given text at the given
# screen dimensions, then stop
def main text:text [
  local-scope
  load-inputs
  open-console
  clear-screen null/screen  # non-scrolling app
  e:&:editor <- new-editor text, 0/left, 5/right
  render null/screen, e
  wait-for-event null/console
  close-console
]

scenario editor-renders-text-to-screen [
  local-scope
  assume-screen 10/width, 5/height
  e:&:editor <- new-editor [abc], 0/left, 10/right
  run [
    render screen, e
  ]
  screen-should-contain [
    # top line of screen reserved for menu
    .          .
    .abc       .
    .          .
  ]
]

container editor [
  # editable text: doubly linked list of characters (head contains a special sentinel)
  data:&:duplex-list:char
  top-of-screen:&:duplex-list:char
  bottom-of-screen:&:duplex-list:char
  # location before cursor inside data
  before-cursor:&:duplex-list:char

  # raw bounds of display area on screen
  # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
  left:num
  right:num
  bottom:num
  # raw screen coordinates of cursor
  cursor-row:num
  cursor-column:num
]

# creates a new editor widget
#   right is exclusive
def new-editor s:text, left:num, right:num -> result:&:editor [
  local-scope
  load-inputs
  # no clipping of bounds
  right <- subtract right, 1
  result <- new editor:type
  # initialize screen-related fields
  *result <- put *result, left:offset, left
  *result <- put *result, right:offset, right
  # initialize cursor coordinates
  *result <- put *result, cursor-row:offset, 1/top
  *result <- put *result, cursor-column:offset, left
  # initialize empty contents
  init:&:duplex-list:char <- push 167/§, null
  *result <- put *result, data:offset, init
  *result <- put *result, top-of-screen:offset, init
  *result <- put *result, before-cursor:offset, init
  result <- insert-text result, s
  <editor-initialization>
]

def insert-text editor:&:editor, text:text -> editor:&:editor [
  local-scope
  load-inputs
  curr:&:duplex-list:char <- get *editor, data:offset
  insert curr, text
]

scenario editor-initializes-without-data [
  local-scope
  assume-screen 5/width, 3/height
  run [
    e:&:editor <- new-editor null/data, 2/left, 5/right
    1:editor/raw <- copy *e
  ]
  memory-should-contain [
    # 1,2 (data) <- just the § sentinel
    # 3,4 (top of screen) <- the § sentinel
    # 5 (bottom of screen) <- null since text fits on screen
    5 <- 0
    6 <- 0
    # 7,8 (before cursor) <- the § sentinel
    9 <- 2  # left
    10 <- 4  # right  (inclusive)
    11 <- 0  # bottom (not set until render)
    12 <- 1  # cursor row
    13 <- 2  # cursor column
  ]
  screen-should-contain [
    .     .
    .     .
    .     .
  ]
]

# Assumes cursor should be at coordinates (cursor-row, cursor-column) and
# updates before-cursor to match. Might also move coordinates if they're
# outside text.
def render screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
  local-scope
  load-inputs
  return-unless editor, 1/top, 0/left
  left:num <- get *editor, left:offset
  screen-height:num <- screen-height screen
  right:num <- get *editor, right:offset
  # traversing editor
  curr:&:duplex-list:char <- get *editor, top-of-screen:offset
  prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
  curr <- next curr
  # traversing screen
  color:num <- copy 7/white
  row:num <- copy 1/top
  column:num <- copy left
  cursor-row:num <- get *editor, cursor-row:offset
  cursor-column:num <- get *editor, cursor-column:offset
  before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
  screen <- move-cursor screen, row, column
  {
    +next-character
    break-unless curr
    off-screen?:bool <- greater-or-equal row, screen-height
    break-if off-screen?
    # update editor.before-cursor
    # Doing so at the start of each iteration ensures it stays one step behind
    # the current character.
    {
      at-cursor-row?:bool <- equal row, cursor-row
      break-unless at-cursor-row?
      at-cursor?:bool <- equal column, cursor-column
      break-unless at-cursor?
      before-cursor <- copy prev
    }
    c:char <- get *curr, value:offset
    <character-c-received>
    {
      # newline? move to left rather than 0
      newline?:bool <- equal c, 10/newline
      break-unless newline?
      # adjust cursor if necessary
      {
        at-cursor-row?:bool <- equal row, cursor-row
        break-unless at-cursor-row?
        left-of-cursor?:bool <- lesser-than column, cursor-column
        break-unless left-of-cursor?
        cursor-column <- copy column
        before-cursor <- prev curr
      }
      # clear rest of line in this window
      clear-line-until screen, right
      # skip to next line
      row <- add row, 1
      column <- copy left
      screen <- move-cursor screen, row, column
      curr <- next curr
      prev <- next prev
      loop +next-character
    }
    {
      # at right? wrap. even if there's only one more letter left; we need
      # room for clicking on the cursor after it.
      at-right?:bool <- equal column, right
      break-unless at-right?
      # print wrap icon
      wrap-icon:char <- copy 8617/loop-back-to-left
      print screen, wrap-icon, 245/grey
      column <- copy left
      row <- add row, 1
      screen <- move-cursor screen, row, column
      # don't increment curr
      loop +next-character
    }
    print screen, c, color
    curr <- next curr
    prev <- next prev
    column <- add column, 1
    loop
  }
  # save first character off-screen
  *editor <- put *editor, bottom-of-screen:offset, curr
  # is cursor to the right of the last line? move to end
  {
    at-cursor-row?:bool <- equal row, cursor-row
    cursor-outside-line?:bool <- lesser-or-equal column, cursor-column
    before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line?
    above-cursor-row?:bool <- lesser-than row, cursor-row
    before-cursor?:bool <- 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
  }
  *editor <- put *editor, bottom:offset, row
  *editor <- put *editor, cursor-row:offset, cursor-row
  *editor <- put *editor, cursor-column:offset, cursor-column
  *editor <- put *editor, before-cursor:offset, before-cursor
  clear-line-until screen, right
  row <- add row, 1
  return row, left/column
]

def clear-screen-from screen:&:screen, row:num, column:num, left:num, right:num -> screen:&:screen [
  local-scope
  load-inputs
  # if it's the real screen, use the optimized primitive
  {
    break-if screen
    clear-display-from row, column, left, right
    return
  }
  # if not, go the slower route
  screen <- move-cursor screen, row, column
  clear-line-until screen, right
  clear-rest-of-screen screen, row, left, right
]

def clear-rest-of-screen screen:&:screen, row:num, left:num, right:num -> screen:&:screen [
  local-scope
  load-inputs
  row <- add row, 1
  # if it's the real screen, use the optimized primitive
  {
    break-if screen
    clear-display-from row, left, left, right
    return
  }
  screen <- move-cursor screen, row, left
  screen-height:num <- screen-height screen
  {
    at-bottom-of-screen?:bool <- greater-or-equal row, screen-height
    break-if at-bottom-of-screen?
    screen <- move-cursor screen, row, left
    clear-line-until screen, right
    row <- add row, 1
    loop
  }
]

scenario editor-prints-multiple-lines [
  local-scope
  assume-screen 5/width, 5/height
  s:text <- new [abc
def]
  e:&:editor <- new-editor s, 0/left, 5/right
  run [
    render screen, e
  ]
  screen-should-contain [
    .     .
    .abc  .
    .def  .
    .     .
  ]
]

scenario editor-handles-offsets [
  local-scope
  assume-screen 5/width, 5/height
  e:&:editor <- new-editor [abc], 1/left, 5/right
  run [
    render screen, e
  ]
  screen-should-contain [
    .     .
    . abc .
    .     .
  ]
]

scenario editor-prints-multiple-lines-at-offset [
  local-scope
  assume-screen 5/width, 5/height
  s:text <- new [abc
def]
  e:&:editor <- new-editor s, 1/left, 5/right
  run [
    render screen, e
  ]
  screen-should-contain [
    .     .
    . abc .
    . def .
    .     .
  ]
]

scenario editor-wraps-long-lines [
  local-scope
  assume-screen 5/width, 5/height
  e:&:editor <- new-editor [abc def], 0/left, 5/right
  run [
    render screen, e
  ]
  screen-should-contain [
    .     .
    .abc .
    .def  .
    .     .
  ]
  screen-should-contain-in-color 245/grey [
    .     .
    .    .
    .     .
    .     .
  ]
]

scenario editor-wraps-barely-long-lines [
  local-scope
  assume-screen 5/width, 5/height
  e:&:editor <- new-editor [abcde], 0/left, 5/right
  run [
    render screen, e
  ]
  # still wrap, even though the line would fit. We need room to click on the
  # end of the line
  screen-should-contain [
    .     .
    .abcd.
    .e    .
    .     .
  ]
  screen-should-contain-in-color 245/grey [
    .     .
    .    .
    .     .
    .     .
  ]
]

scenario editor-with-empty-text [
  local-scope
  assume-screen 5/width, 5/height
  e:&:editor <- new-editor [], 0/left, 5/right
  run [
    render screen, e
    3:num/raw <- get *e, cursor-row:offset
    4:num/raw <- get *e, cursor-column:offset
  ]
  screen-should-contain [
    .     .
    .     .
    .     .
  ]
  memory-should-contain [
    3 <- 1  # cursor row
    4 <- 0  # cursor column
  ]
]

# just a little color for Mu code

scenario render-colors-comments [
  local-scope
  assume-screen 5/width, 5/height
  s:text <- new [abc
# de
f]
  e:&:editor <- new-editor s, 0/left, 5/right
  run [
    render screen, e
  ]
  screen-should-contain [
    .     .
    .abc  .
    .# de .
    .f    .
    .     .
  ]
  screen-should-contain-in-color 12/lightblue, [
    .     .
    .     .
    .# de .
    .     .
    .     .
  ]
  screen-should-contain-in-color 7/white, [
    .     .
    .abc  .
    .     .
    .f    .
    .     .
  ]
]

after <character-c-received> [
  color <- get-color color, c
]

# so far the previous color is all the information we need; that may change
def get-color color:num, c:char -> color:num [
  local-scope
  load-inputs
  color-is-white?:bool <- equal color, 7/white
  # if color is white and next character is '#', switch color to blue
  {
    break-unless color-is-white?
    starting-comment?:bool <- equal c, 35/#
    break-unless starting-comment?
    trace 90, [app], [switch color back to blue]
    return 12/lightblue
  }
  # if color is blue and next character is newline, switch color to white
  {
    color-is-blue?:bool <- equal color, 12/lightblue
    break-unless color-is-blue?
    ending-comment?:bool <- equal c, 10/newline
    break-unless ending-comment?
    trace 90, [app], [switch color back to white]
    return 7/white
  }
  # if color is white (no comments) and next character is '<', switch color to red
  {
    break-unless color-is-white?
    starting-assignment?:bool <- equal c, 60/<
    break-unless starting-assignment?
    return 1/red
  }
  # if color is red and next character is space, switch color to white
  {
    color-is-red?:bool <- equal color, 1/red
    break-unless color-is-red?
    ending-assignment?:bool <- equal c, 32/space
    break-unless ending-assignment?
    return 7/white
  }
  # otherwise no change
  return color
]

scenario render-colors-assignment [
  local-scope
  assume-screen 8/width, 5/height
  s:text <- new [abc
d <- e
f]
  e:&:editor <- new-editor s, 0/left, 8/right
  run [
    render screen, e
  ]
  screen-should-contain [
    .        .
    .abc     .
    .d <- e  .
    .f       .
    .        .
  ]
  screen-should-contain-in-color 1/red, [
    .        .
    .        .
    .  <-    .
    .        .
    .        .
  ]
]
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<
# Helper to allocate a stream on the heap.

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes

new-stream:  # ad : (address allocation-descriptor), length : int, elemsize : int -> address/EAX
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    52/push-EDX
    # n = elemsize * length + 12 (for read, write and length)
    # . EAX = elemsize
    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0x10/disp8      .                 # copy *(EBP+16) to EAX
    # . EAX *= length
    31/xor                          3/mod/direct    2/rm32/EDX    .           .             .           2/r32/EDX   .               .                 # clear EDX
    f7          4/subop/multiply    1/mod/*+disp8   5/rm32/EBP    .           .                                     0xc/disp8       .                 # multiply *(EBP+12) into EAX
    # . if overflow abort
    81          7/subop/compare     3/mod/direct    2/rm32/EDX    .           .             .           .           .               0/imm32           # compare EDX
    75/jump-if-not-equal  $new-stream:abort/disp8
    # . EDX = elemsize*length
    89/copy                         3/mod/direct    2/rm32/EDX    .           .             .           0/r32/EAX   .               .                 # copy EAX to EDX
    # . EAX += 12
    05/add-to-EAX  0xc/imm32
    # allocate(ad, n)
    # . . push args
    50/push-EAX
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  allocate/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # EAX->length = elemsize*length
    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           2/r32/EDX   8/disp8         .                 # copy EDX to *(EAX+8)
    # clear-stream(EAX)
    # . . push args
    50/push-EAX
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
$new-stream:end:
    # . restore registers
    5a/pop-to-EDX
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

$new-stream:abort:
    # . _write(2/stderr, error)
    # . . push args
    68/push  "new-stream: size too large"/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . syscall(exit, 1)
    bb/copy-to-EBX  1/imm32
    b8/copy-to-EAX  1/imm32/exit
    cd/syscall  0x80/imm8
    # never gets here

test-new-stream:
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # var heap/ECX : (address allocation-descriptor) = {0, 0}
    68/push  0/imm32/limit
    68/push  0/imm32/curr
    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
    # heap = new-segment(512)
    # . . push args
    51/push-ECX
    68/push  0x200/imm32
    # . . call
    e8/call  new-segment/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # var start/EDX = ad->curr
    8b/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           2/r32/EDX   .               .                 # copy *ECX to EDX
    # EAX = new-stream(heap, 3, 2)
    # . . push args
    68/push  2/imm32
    68/push  3/imm32
    51/push-ECX
    # . . call
    e8/call  new-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # check-ints-equal(EAX, EDX, msg)
    # . . push args
    68/push  "F - test-new-stream: returns current pointer of allocation descriptor"/imm32
    52/push-EDX
    50/push-EAX
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # check-ints-equal(EAX->length, 6, msg)
    # . . push args
    68/push  "F - test-new-stream: sets length correctly"/imm32
    68/push  6/imm32
    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           .               8/disp8           # push *(EAX+8)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # the rest is delegated to clear-stream() so we won't bother checking it
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

# . . vim:nowrap:textwidth=0