src/lsp/enc.bend
bolt@1.11.0.0 · 207 lines · 0 definitions
1# lsp/enc: position encodings (LSP 3.17 `positionEncoding`), the one place a
2# character offset crosses the protocol boundary. Pure. bolt counts columns in
3# code points (a Bend String is a list of them); a client that offers
4# `utf-32` gets exactly those, and any other gets UTF-16 code units, where a
5# char outside the Basic Multilingual Plane counts 2. Every position the
6# server reads goes through `into`, and every one it sends through `out`
7# (proto.bend's position, semantic.bend's tokens).
8import Base
9import bolt@1.11.0.0/src/lazy/lazy.bend as Lazy
10import emerging-ezjson@1.1.0.0/main.bend as Ezjson
11import emerging-ezjson@1.1.0.0/src/value.bend as J
12
13# the encodings the server speaks
14type Enc is Data:
15 Utf16{}
16 Utf32{}
17
18# an encoding's name on the wire
19def name(ee: Enc) -> String:
20 match ee:
21 case Utf16{}:
22 "utf-16"
23 case Utf32{}:
24 "utf-32"
25
26# does a list of encodings (JSON cells) offer utf-32? A cell that is not a
27# string offers nothing
28def offers(cells: J.Json) -> Bool:
29 match cells:
30 case J.JCons{h, t}:
31 +more = offers(t)
32 Bool.or(String.eq(Maybe.default(&2, String, Ezjson.as_str(h), ""), "utf-32"), more)
33 case _other:
34 False{}
35
36# the cells of an array, JNil when it is not one
37def cells(jj: J.Json) -> J.Json:
38 match jj:
39 case J.JArr{items}:
40 items
41 case _other:
42 J.JNil{}
43
44# the encoding for utf-32 offered or not
45def chosen(yes: Bool) -> Enc:
46 match yes:
47 case True{}:
48 Utf32{}
49 case False{}:
50 Utf16{}
51
52# the encoding initialize settles on: utf-32 when the client's
53# general.positionEncodings offers it, else UTF-16, LSP's default
54def negotiate(params: J.Json) -> Enc:
55 chosen(offers(cells(Ezjson.get(Ezjson.get(Ezjson.get(params, "capabilities"), "general"), "positionEncodings"))))
56
57# counting
58# --------
59
60# does a char take two UTF-16 code units (it lies past U+FFFF)?
61def wide(cc: Char) -> Bool:
62 U32.is_le(65536, Char.to_u32(cc))
63
64# the units a char and the ones after it take: two for a wide char, else one
65def units.step(isw: Bool, rest: Nat) -> Nat:
66 match isw:
67 case True{}:
68 2n+rest
69 case False{}:
70 1n+rest
71
72# the UTF-16 code units the first n chars of a line take; a column past the
73# line's end counts one unit a column
74def units(cs: List<&2, Char>, nn: Nat) -> Nat:
75 match cs nn:
76 case Nil{} _:
77 nn
78 case Con{_c, _t} 0n:
79 0n
80 case Con{c, t} 1n+p:
81 units.step(wide(c), units(t, p))
82
83# the units left after a char, from 1+p units: p after a narrow one, one
84# fewer after a wide one (none when the offset falls inside its pair)
85def points.skip(isw: Bool, pp: Nat) -> Nat:
86 match isw pp:
87 case True{} 0n:
88 0n
89 case True{} 1n+q:
90 q
91 case False{} _:
92 pp
93
94# the chars the first u UTF-16 code units of a line cover; an offset inside
95# a pair covers its char, and one past the line's end counts one column a
96# unit
97def points(cs: List<&2, Char>, uu: Nat) -> Nat:
98 match cs uu:
99 case Nil{} _:
100 uu
101 case Con{_c, _t} 0n:
102 0n
103 case Con{c, t} 1n+p:
104 1n+points(t, points.skip(wide(c), p))
105
106# a code-point column of a line in an encoding
107def out.of(ee: Enc, cs: List<&2, Char>, nn: Nat) -> Nat:
108 match ee:
109 case Utf16{}:
110 units(cs, nn)
111 case Utf32{}:
112 nn
113
114# an encoding's column of a line as a code-point column
115def into.of(ee: Enc, cs: List<&2, Char>, uu: Nat) -> Nat:
116 match ee:
117 case Utf16{}:
118 points(cs, uu)
119 case Utf32{}:
120 uu
121
122# does a line hold a char past U+FFFF? Stops at the first
123def wide_in(cs: List<&2, Char>) -> Bool:
124 match cs:
125 case Nil{}:
126 False{}
127 case Con{c, t}:
128 Lazy.or_else(wide(c), _u => wide_in(t))
129
130# does any line hold one? Stops at the first
131def wide_any(lines: List<&2, String>) -> Bool:
132 match lines:
133 case Nil{}:
134 False{}
135 case Con{l, t}:
136 Lazy.or_else(wide_in(String.to_list(l)), _u => wide_any(t))
137
138# the encoding a document's columns convert by: UTF-16 on lines with no wide
139# char counts exactly as utf-32 does (LAWS.bend's enc_narrow), so such a
140# document converts as utf-32 and never looks a line up
141def narrowed(ee: Enc, wide: Bool) -> Enc:
142 match ee:
143 case Utf16{}:
144 Bool.pick(Enc, wide, Utf16{}, Utf32{})
145 case Utf32{}:
146 Utf32{}
147
148# a document as the boundary sees it: the encoding its columns convert by
149# and the text's lines
150type Cols is Data:
151 Cols{enc: Enc, lines: List<&2, String>}
152
153# a document's lines under an encoding
154def cols.of(ee: Enc, +lines: List<&2, String>) -> Cols:
155 Cols{narrowed(ee, wide_any(lines)), lines}
156
157# a document's text under an encoding
158def cols(ee: Enc, text: String) -> Cols:
159 cols.of(ee, String.lines(text))
160
161# a line's chars, none past the end
162def chars(lines: List<&2, String>, line: U32) -> List<&2, Char>:
163 String.to_list(Maybe.default(&2, String, List.get(&2, String, lines, U32.to_nat(line)), ""))
164
165# a code-point column of a line's chars, sent in an encoding
166def out.col(ee: Enc, cs: List<&2, Char>, col: U32) -> U32:
167 match ee:
168 case Utf16{}:
169 U32.from_nat(out.of(Utf16{}, cs, U32.to_nat(col)))
170 case Utf32{}:
171 col
172
173# a code-point column of a document's line, sent in an encoding; utf-32
174# never looks the line up
175def out.at(ee: Enc, lines: List<&2, String>, line: U32, col: U32) -> U32:
176 match ee:
177 case Utf16{}:
178 out.col(Utf16{}, chars(lines, line), col)
179 case Utf32{}:
180 col
181
182# a code-point column sent in the negotiated encoding
183def out(cc: Cols, line: U32, col: U32) -> U32:
184 Cols{ee, lines} = cc
185 out.at(ee, lines, line, col)
186
187# a column of a line's chars read in an encoding, as a code-point column
188def into.col(ee: Enc, cs: List<&2, Char>, col: U32) -> U32:
189 match ee:
190 case Utf16{}:
191 U32.from_nat(into.of(Utf16{}, cs, U32.to_nat(col)))
192 case Utf32{}:
193 col
194
195# a column of a document's line read in an encoding; utf-32 never looks the
196# line up
197def into.at(ee: Enc, lines: List<&2, String>, line: U32, col: U32) -> U32:
198 match ee:
199 case Utf16{}:
200 into.col(Utf16{}, chars(lines, line), col)
201 case Utf32{}:
202 col
203
204# a column read in the negotiated encoding, as a code-point column
205def into(cc: Cols, line: U32, col: U32) -> U32:
206 Cols{ee, lines} = cc
207 into.at(ee, lines, line, col)