1 # src/syntax/word: the name under a (line, col), both 0-based. A name is letters, 2 # digits, `_` and `.` (dots are just characters in a Bend name, and a module 3 # alias rides in front: `Frame.decode`). A position right after a name counts 4 # as on it, as editors put the cursor there. 5 import Base 6 import ../lazy/lazy.bend as Lazy 7 import ../rchars.bend as Rchars 8 9 # i: the column of the next char; start: where the current run began; run: the 10 # current name, reversed; hit: the name found 11 type W is Data: 12 W{i: U32, start: U32, run: List<&2, Char>, hit: Maybe<&2, String>} 13 14 # a char a name may hold: letters, digits, `_` and `.` 15 def is_name(+cc: Char) -> Bool: 16 Bool.or(Bool.or(Char.is_alpha(cc), Char.is_digit(cc)), Bool.or(Char.is_eq(cc, '_'), Char.is_eq(cc, '.'))) 17 18 # the run's chars (reversed) as a string 19 def text(run: List<&2, Char>) -> String: 20 Rchars.text(run) 21 22 # the run holds the column: keep it; otherwise the hit so far 23 def close.yes(ok: Bool, run: List<&2, Char>, hit: Maybe<&2, String>) -> Maybe<&2, String>: 24 match ok: 25 case True{}: 26 Some{text(run)} 27 case False{}: 28 hit 29 30 # an empty run leaves the hit; otherwise the run if start <= col <= i 31 def close.run(run: List<&2, Char>, +start: U32, +ii: U32, +col: U32, hit: Maybe<&2, String>) -> Maybe<&2, String>: 32 match run: 33 case Nil{}: 34 hit 35 case h <> t: 36 close.yes(Bool.and(U32.is_le(start, col), U32.is_le(col, ii)), h <> t, hit) 37 38 # a run that ends at column i holds col when start <= col <= i, unless one already did 39 def close(+ii: U32, +start: U32, run: List<&2, Char>, hit: Maybe<&2, String>, +col: U32) -> Maybe<&2, String>: 40 match hit: 41 case Some{s}: 42 Some{s} 43 case None{}: 44 close.run(run, start, ii, col, None{}) 45 46 # a name char joins the run; any other char ends it 47 def step.on(name: Bool, cc: Char, +ii: U32, +start: U32, run: List<&2, Char>, hit: Maybe<&2, String>, +col: U32) -> W: 48 match name: 49 case True{}: 50 W{(ii + 1 : U32), start, cc <> run, hit} 51 case False{}: 52 W{(ii + 1 : U32), (ii + 1 : U32), [], close(ii, start, run, hit, col)} 53 54 # one char: it extends the run of name chars, or ends it (a run holding the 55 # column is the answer) 56 def step(+cc: Char, st: W, +col: U32) -> W: 57 W{i, start, run, hit} = st 58 step.on(is_name(cc), cc, i, start, run, hit, col) 59 60 # the run that held the column, else "" 61 def finish(st: W, col: U32) -> String: 62 W{i, start, run, hit} = st 63 Maybe.default(&2, String, close(i, start, run, hit, col), "") 64 65 # lines still to skip after this char; a newline counts as one 66 def at.skip.n(nl: Bool, nn: U32) -> U32: 67 match nl: 68 case True{}: 69 (nn - 1 : U32) 70 case False{}: 71 nn 72 73 # the next char starts the wanted line when this one was its preceding newline 74 def at.skip.done(nl: Bool, nn: U32) -> Bool: 75 match nl: 76 case True{}: 77 U32.is_eq(nn, 1) 78 case False{}: 79 False{} 80 81 # the source with `n` lines skipped. `done` means this string is the line 82 def at.skip(ss: String, +nn: U32, done: Bool) -> String: 83 match ss done: 84 case SNil{} _: 85 SNil{} 86 case SCon{h, t} True{}: 87 SCon{h, t} 88 case SCon{+h, t} False{}: 89 at.skip(t, at.skip.n(Char.is_eq(h, '\n'), nn), at.skip.done(Char.is_eq(h, '\n'), nn)) 90 91 # a newline ends the line; any other char is part of it 92 def at.advance(nl: Bool, hh: Char, st: W, +col: U32) -> W: 93 match nl: 94 case True{}: 95 st 96 case False{}: 97 step(hh, st, col) 98 99 # the name at col, from here up to the next newline 100 def at.read(ss: String, stop: Bool, st: W, +col: U32) -> String: 101 match ss stop: 102 case SNil{} _: 103 finish(st, col) 104 case SCon{h, t} True{}: 105 finish(st, col) 106 case SCon{+h, t} False{}: 107 at.read(t, Char.is_eq(h, '\n'), at.advance(Char.is_eq(h, '\n'), h, st, col), col) 108 109 # the name under a (line, col) of a source, "" when none 110 def at(source: String, +line: U32, col: U32) -> String: 111 at.read(at.skip(source, line, U32.is_eq(line, 0)), False{}, W{0, 0, [], None{}}, col) 112 113 # `Alias.rest` cut at its first dot: ("Alias", "rest"); no dot: (name, "") 114 def cut.go(cs: List<&2, Char>, acc: List<&2, Char>) -> String & String: 115 match cs: 116 case Nil{}: 117 (text(acc), "") 118 case Con{'.', t}: 119 (text(acc), String.from_list(t)) 120 case Con{c, t}: 121 cut.go(t, c <> acc) 122 123 # `Alias.rest` cut at its first dot: ("Alias", "rest"); no dot: (name, "") 124 def cut(name: String) -> String & String: 125 cut.go(String.to_list(name), []) 126 127 # a pair's first 128 def fst(pp: String & String) -> String: 129 (h, r) = pp 130 h 131 132 # a pair's second 133 def snd(pp: String & String) -> String: 134 (h, r) = pp 135 r 136 137 # the part before the first dot 138 def head(name: String) -> String: 139 fst(cut(name)) 140 141 # the part after the first dot, "" when there is none 142 def rest(name: String) -> String: 143 snd(cut(name)) 144 145 # the leading run of name chars 146 def run_of(cs: List<&2, Char>) -> List<&2, Char>: 147 match cs: 148 case Nil{}: 149 Nil{} 150 case Con{+c, t}: 151 Lazy.stop(List<&2, Char>, Bool.not(is_name(c)), Nil{}, _u => c <> run_of(t)) 152 153 # the name that ends at (line, col): what is being typed, for completion 154 def before(source: String, line: U32, col: U32) -> String: 155 text(run_of(List.reverse(&2, Char, String.to_list(String.take( 156 Maybe.default(&2, String, List.get(&2, String, String.lines(source), U32.to_nat(line)), ""), 157 U32.to_nat(col))))))