src/syntax/word.bend

bolt@1.11.0.0 · 156 lines · 0 definitions

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