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))))))