src/syntax/word.bend

bolt@1.20.0.0 · 157 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.20.0.0/src/lazy/lazy.bend as Lazy
7import bolt@1.20.0.0/src/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
11type 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 `.`
15def 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
19def text(run: List<&2, Char>) -> String:
20  Rchars.text(run)
21
22# the run holds the column: keep it; otherwise the hit so far
23def 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
31def 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
39def 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
47def 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)
56def 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 ""
61def 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
66def 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
74def 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
82def 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
92def 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
100def 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
110def 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, "")
114def 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, "")
124def cut(name: String) -> String & String:
125  cut.go(String.to_list(name), [])
126
127# a pair's first
128def fst(pp: String & String) -> String:
129  (h, r) = pp
130  h
131
132# a pair's second
133def snd(pp: String & String) -> String:
134  (h, r) = pp
135  r
136
137# the part before the first dot
138def head(name: String) -> String:
139  fst(cut(name))
140
141# the part after the first dot, "" when there is none
142def rest(name: String) -> String:
143  snd(cut(name))
144
145# the leading run of name chars
146def 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
154def 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))))))