@forinda/kickjs-view-language-server 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +31 -2
- package/bin/kick-view-ls.js +2 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +18 -0
- package/dist/main.js.map +1 -0
- package/dist/server-3edMz3HM.js +1157 -0
- package/dist/server-3edMz3HM.js.map +1 -0
- package/package.json +63 -3
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs-view-language-server v0.1.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Felix Orinda
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
14
|
+
import { createConnection, createServer, createSimpleProject } from "@volar/language-server/node.js";
|
|
15
|
+
import { create } from "volar-service-css";
|
|
16
|
+
import { create as create$1 } from "volar-service-emmet";
|
|
17
|
+
import { create as create$2 } from "volar-service-html";
|
|
18
|
+
import { URI } from "vscode-uri";
|
|
19
|
+
import { pathToFileURL } from "node:url";
|
|
20
|
+
import * as bundled from "@forinda/kickjs-view";
|
|
21
|
+
//#region src/language-plugin.ts
|
|
22
|
+
const ALL = {
|
|
23
|
+
verification: true,
|
|
24
|
+
completion: true,
|
|
25
|
+
semantic: true,
|
|
26
|
+
navigation: true,
|
|
27
|
+
structure: true,
|
|
28
|
+
format: true
|
|
29
|
+
};
|
|
30
|
+
/** Embedded documents never format: the kick formatter owns the whole file. */
|
|
31
|
+
const EMBEDDED = {
|
|
32
|
+
...ALL,
|
|
33
|
+
format: false
|
|
34
|
+
};
|
|
35
|
+
const KEYWORD$1 = /^@(else\s+if|if|for|switch|case|slot|layout|assets|verbatim|let|input|else|empty|default)(?![\w$-])/;
|
|
36
|
+
const snapshotOf = (text) => ({
|
|
37
|
+
getText: (start, end) => text.slice(start, end),
|
|
38
|
+
getLength: () => text.length,
|
|
39
|
+
getChangeRange: () => void 0
|
|
40
|
+
});
|
|
41
|
+
/** The end of a `(`…`)` group starting at `i` (at `(`); the text's end when unclosed. */
|
|
42
|
+
function closeParen$1(s, i) {
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let quote = "";
|
|
45
|
+
for (let j = i; j < s.length; j++) {
|
|
46
|
+
const c = s[j];
|
|
47
|
+
if (quote) {
|
|
48
|
+
if (c === "\\") j++;
|
|
49
|
+
else if (c === quote) quote = "";
|
|
50
|
+
} else if (c === "'" || c === "\"") quote = c;
|
|
51
|
+
else if (c === "(") depth++;
|
|
52
|
+
else if (c === ")" && --depth === 0) return j + 1;
|
|
53
|
+
}
|
|
54
|
+
return s.length;
|
|
55
|
+
}
|
|
56
|
+
/** The end of a `{{ … }}` starting at `i`; the text's end when unclosed. */
|
|
57
|
+
function closeInterp(s, i) {
|
|
58
|
+
let quote = "";
|
|
59
|
+
for (let j = i + 2; j < s.length; j++) {
|
|
60
|
+
const c = s[j];
|
|
61
|
+
if (quote) {
|
|
62
|
+
if (c === "\\") j++;
|
|
63
|
+
else if (c === quote) quote = "";
|
|
64
|
+
} else if (c === "'" || c === "\"") quote = c;
|
|
65
|
+
else if (c === "}" && s[j + 1] === "}") return j + 2;
|
|
66
|
+
}
|
|
67
|
+
return s.length;
|
|
68
|
+
}
|
|
69
|
+
function scan(s) {
|
|
70
|
+
const out = {
|
|
71
|
+
blank: [],
|
|
72
|
+
styles: [],
|
|
73
|
+
scripts: []
|
|
74
|
+
};
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let i = 0;
|
|
77
|
+
const skipWs = (j) => {
|
|
78
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
79
|
+
return j;
|
|
80
|
+
};
|
|
81
|
+
while (i < s.length) {
|
|
82
|
+
const c = s[i];
|
|
83
|
+
if (c === "{" && s.startsWith("{{--", i)) {
|
|
84
|
+
const end = s.indexOf("--}}", i + 4);
|
|
85
|
+
const stop = end < 0 ? s.length : end + 4;
|
|
86
|
+
out.blank.push([i, stop]);
|
|
87
|
+
i = stop;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === "{" && s[i + 1] === "{") {
|
|
91
|
+
const stop = closeInterp(s, i);
|
|
92
|
+
out.blank.push([i, stop]);
|
|
93
|
+
i = stop;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (c === "<" && s.startsWith("<!--", i)) {
|
|
97
|
+
const end = s.indexOf("-->", i + 4);
|
|
98
|
+
i = end < 0 ? s.length : end + 3;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (c === "<" && /^<\/?[A-Za-z]/.test(s.slice(i, i + 3))) {
|
|
102
|
+
i = tag(s, i, out);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c === "@" && !/[\w@.-]/.test(s[i - 1] ?? "")) {
|
|
106
|
+
const m = KEYWORD$1.exec(s.slice(i, i + 16));
|
|
107
|
+
if (m) {
|
|
108
|
+
const kw = m[1].replace(/\s+/g, " ");
|
|
109
|
+
let j = i + m[0].length;
|
|
110
|
+
if (kw === "let" || kw === "input") {
|
|
111
|
+
const semi = kw === "let" ? s.indexOf(";", j) : -1;
|
|
112
|
+
const eol = s.indexOf("\n", j);
|
|
113
|
+
const stop = semi >= 0 && (eol < 0 || semi < eol) ? semi + 1 : eol < 0 ? s.length : eol;
|
|
114
|
+
out.blank.push([i, stop]);
|
|
115
|
+
i = stop;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (kw === "verbatim") {
|
|
119
|
+
const b = skipWs(j);
|
|
120
|
+
if (s[b] === "{") {
|
|
121
|
+
let d = 0;
|
|
122
|
+
let k = b;
|
|
123
|
+
for (; k < s.length; k++) if (s[k] === "{") d++;
|
|
124
|
+
else if (s[k] === "}" && --d === 0) break;
|
|
125
|
+
const stop = Math.min(k + 1, s.length);
|
|
126
|
+
out.blank.push([i, stop]);
|
|
127
|
+
i = stop;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const p = skipWs(j);
|
|
132
|
+
if (s[p] === "(") j = closeParen$1(s, p);
|
|
133
|
+
else if (kw !== "else" && kw !== "empty" && kw !== "default") {
|
|
134
|
+
i++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (kw === "layout" || kw === "assets") {
|
|
138
|
+
out.blank.push([i, j]);
|
|
139
|
+
i = j;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const b = skipWs(j);
|
|
143
|
+
if (s[b] === "{") {
|
|
144
|
+
out.blank.push([i, b + 1]);
|
|
145
|
+
depth++;
|
|
146
|
+
i = b + 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
out.blank.push([i, j]);
|
|
150
|
+
i = j;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (c === "}" && depth > 0) {
|
|
155
|
+
out.blank.push([i, i + 1]);
|
|
156
|
+
depth--;
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
/** One tag from `i` (at `<`): interpolations and binding values blanked; script / style content recorded. */
|
|
165
|
+
function tag(s, i, out) {
|
|
166
|
+
const m = /^<(\/?)([A-Za-z][\w:.-]*)/.exec(s.slice(i, i + 64));
|
|
167
|
+
let j = i + m[0].length;
|
|
168
|
+
const closing = !!m[1];
|
|
169
|
+
const name = m[2].toLowerCase();
|
|
170
|
+
let type = "";
|
|
171
|
+
while (j < s.length && s[j] !== ">") {
|
|
172
|
+
const binding = /^\[[^\]\s]+\]\s*=\s*(["'])/.exec(s.slice(j, j + 128));
|
|
173
|
+
if (binding) {
|
|
174
|
+
const q = binding[1];
|
|
175
|
+
const from = j + binding[0].length;
|
|
176
|
+
const end = s.indexOf(q, from);
|
|
177
|
+
const stop = end < 0 ? s.length : end;
|
|
178
|
+
out.blank.push([from, stop]);
|
|
179
|
+
j = stop + 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const t = /^type\s*=\s*(["'])([^"']*)\1/i.exec(s.slice(j, j + 128));
|
|
183
|
+
if (t) type = t[2].trim().toLowerCase();
|
|
184
|
+
const q = s[j];
|
|
185
|
+
if (q === "\"" || q === "'") {
|
|
186
|
+
const end = s.indexOf(q, j + 1);
|
|
187
|
+
const stop = end < 0 ? s.length : end;
|
|
188
|
+
for (let k = j + 1; k < stop; k++) if (s[k] === "{" && s[k + 1] === "{") {
|
|
189
|
+
const e = Math.min(closeInterp(s, k), stop);
|
|
190
|
+
out.blank.push([k, e]);
|
|
191
|
+
k = e - 1;
|
|
192
|
+
}
|
|
193
|
+
j = stop + 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
j++;
|
|
197
|
+
}
|
|
198
|
+
j = Math.min(j + 1, s.length);
|
|
199
|
+
if (closing || name !== "script" && name !== "style" || s[j - 2] === "/") return j;
|
|
200
|
+
const close = new RegExp(`</${name}(?=[\\t\\n\\f\\r />])`, "i").exec(s.slice(j));
|
|
201
|
+
const end = close ? j + close.index : s.length;
|
|
202
|
+
for (let k = j; k < end; k++) if (s[k] === "{" && s[k + 1] === "{") {
|
|
203
|
+
const e = Math.min(closeInterp(s, k), end);
|
|
204
|
+
out.blank.push([k, e]);
|
|
205
|
+
k = e - 1;
|
|
206
|
+
}
|
|
207
|
+
if (name === "style") out.styles.push([j, end]);
|
|
208
|
+
else if (type !== "application/json" && type !== "application/ld+json") out.scripts.push([j, end]);
|
|
209
|
+
return end;
|
|
210
|
+
}
|
|
211
|
+
function embedded(id, languageId, text, [from, to]) {
|
|
212
|
+
return {
|
|
213
|
+
id,
|
|
214
|
+
languageId,
|
|
215
|
+
snapshot: snapshotOf(text.slice(from, to)),
|
|
216
|
+
mappings: [{
|
|
217
|
+
sourceOffsets: [from],
|
|
218
|
+
generatedOffsets: [0],
|
|
219
|
+
lengths: [to - from],
|
|
220
|
+
data: EMBEDDED
|
|
221
|
+
}]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
var KickVirtualCode = class {
|
|
225
|
+
text;
|
|
226
|
+
id = "root";
|
|
227
|
+
languageId = "kick";
|
|
228
|
+
snapshot;
|
|
229
|
+
mappings;
|
|
230
|
+
embeddedCodes;
|
|
231
|
+
constructor(text) {
|
|
232
|
+
this.text = text;
|
|
233
|
+
this.snapshot = snapshotOf(text);
|
|
234
|
+
this.mappings = [{
|
|
235
|
+
sourceOffsets: [0],
|
|
236
|
+
generatedOffsets: [0],
|
|
237
|
+
lengths: [text.length],
|
|
238
|
+
data: ALL
|
|
239
|
+
}];
|
|
240
|
+
const { blank, styles, scripts } = scan(text);
|
|
241
|
+
const sorted = blank.toSorted((a, b) => a[0] - b[0]);
|
|
242
|
+
let html = "";
|
|
243
|
+
const htmlMappings = [];
|
|
244
|
+
let at = 0;
|
|
245
|
+
const keep = (from, to) => {
|
|
246
|
+
if (to <= from) return;
|
|
247
|
+
html += text.slice(from, to);
|
|
248
|
+
htmlMappings.push({
|
|
249
|
+
sourceOffsets: [from],
|
|
250
|
+
generatedOffsets: [from],
|
|
251
|
+
lengths: [to - from],
|
|
252
|
+
data: EMBEDDED
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
for (const [a, b] of sorted) {
|
|
256
|
+
if (a < at) {
|
|
257
|
+
if (b > at) {
|
|
258
|
+
html += text.slice(at, b).replace(/[^\n\r]/g, " ");
|
|
259
|
+
at = b;
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
keep(at, a);
|
|
264
|
+
html += text.slice(a, b).replace(/[^\n\r]/g, " ");
|
|
265
|
+
at = b;
|
|
266
|
+
}
|
|
267
|
+
keep(at, text.length);
|
|
268
|
+
this.embeddedCodes = [
|
|
269
|
+
{
|
|
270
|
+
id: "html",
|
|
271
|
+
languageId: "html",
|
|
272
|
+
snapshot: snapshotOf(html),
|
|
273
|
+
mappings: htmlMappings
|
|
274
|
+
},
|
|
275
|
+
...styles.map((r, n) => embedded(`css_${n}`, "css", text, r)),
|
|
276
|
+
...scripts.map((r, n) => embedded(`js_${n}`, "javascript", text, r))
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const kickLanguagePlugin = {
|
|
281
|
+
getLanguageId(uri) {
|
|
282
|
+
return uri.path.endsWith(".kick") ? "kick" : void 0;
|
|
283
|
+
},
|
|
284
|
+
createVirtualCode(_uri, languageId, snapshot) {
|
|
285
|
+
return languageId === "kick" ? new KickVirtualCode(snapshot.getText(0, snapshot.getLength())) : void 0;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/context.ts
|
|
290
|
+
const LOOP_VARS = [
|
|
291
|
+
"$index",
|
|
292
|
+
"$first",
|
|
293
|
+
"$last",
|
|
294
|
+
"$even",
|
|
295
|
+
"$odd",
|
|
296
|
+
"$count"
|
|
297
|
+
];
|
|
298
|
+
const IDENT = /[A-Za-z_$][\w$]*/;
|
|
299
|
+
/** The end of `(`…`)` from `i` (at `(`), or -1 when it doesn't close before `stop`. */
|
|
300
|
+
function closeParen(s, i, stop) {
|
|
301
|
+
let depth = 0;
|
|
302
|
+
let quote = "";
|
|
303
|
+
for (let j = i; j < stop; j++) {
|
|
304
|
+
const c = s[j];
|
|
305
|
+
if (quote) {
|
|
306
|
+
if (c === "\\") j++;
|
|
307
|
+
else if (c === quote) quote = "";
|
|
308
|
+
} else if (c === "'" || c === "\"") quote = c;
|
|
309
|
+
else if (c === "(") depth++;
|
|
310
|
+
else if (c === ")" && --depth === 0) return j + 1;
|
|
311
|
+
}
|
|
312
|
+
return -1;
|
|
313
|
+
}
|
|
314
|
+
/** Names a block header brings into its body. */
|
|
315
|
+
function headerNames(kw, header) {
|
|
316
|
+
if (kw === "for") {
|
|
317
|
+
const [head, ...rest] = header.split(";");
|
|
318
|
+
const m = /^\s*(?:\[\s*([\w$]+)\s*,\s*([\w$]+)\s*\]|([\w$]+))\s+of\b/.exec(head ?? "");
|
|
319
|
+
const names = m ? m[3] ? [m[3]] : [m[1], m[2]] : [];
|
|
320
|
+
for (const part of rest) {
|
|
321
|
+
const lets = /^\s*let\s+(.*)$/.exec(part);
|
|
322
|
+
if (lets) for (const d of lets[1].split(",")) names.push(d.split("=")[0].trim());
|
|
323
|
+
}
|
|
324
|
+
return [...names.filter(Boolean), ...LOOP_VARS];
|
|
325
|
+
}
|
|
326
|
+
if (kw === "if" || kw === "else if") {
|
|
327
|
+
const as = /;\s*as\s+([\w$]+)/.exec(header);
|
|
328
|
+
return as ? [as[1]] : [];
|
|
329
|
+
}
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
/** Names in scope at `stop`: inputs, then each enclosing block's, outermost first. */
|
|
333
|
+
function scopeAt(s, stop) {
|
|
334
|
+
const inputs = [];
|
|
335
|
+
const frames = [[]];
|
|
336
|
+
let i = 0;
|
|
337
|
+
while (i < stop) {
|
|
338
|
+
const c = s[i];
|
|
339
|
+
if (c === "{" && s[i + 1] === "{") {
|
|
340
|
+
const end = s.indexOf(s.startsWith("{{--", i) ? "--}}" : "}}", i + 2);
|
|
341
|
+
if (end < 0 || end >= stop) break;
|
|
342
|
+
i = end + 2;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (c === "<") {
|
|
346
|
+
const end = s.startsWith("<!--", i) ? s.indexOf("-->", i) : s.indexOf(">", i);
|
|
347
|
+
if (end < 0 || end >= stop) break;
|
|
348
|
+
i = end + 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (c === "@" && !/[\w@.-]/.test(s[i - 1] ?? "")) {
|
|
352
|
+
const m = /^@(else\s+if|for|if|switch|case|slot|else|empty|default|let|input)(?![\w$-])/.exec(s.slice(i, i + 16));
|
|
353
|
+
if (m) {
|
|
354
|
+
const kw = m[1].replace(/\s+/g, " ");
|
|
355
|
+
let j = i + m[0].length;
|
|
356
|
+
if (kw === "input") {
|
|
357
|
+
const name = IDENT.exec(s.slice(j, j + 64));
|
|
358
|
+
if (name) inputs.push(name[0]);
|
|
359
|
+
i = j;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (kw === "let") {
|
|
363
|
+
const name = /^\s+([\w$]+)\s*=/.exec(s.slice(j, j + 64));
|
|
364
|
+
const semi = s.indexOf(";", j);
|
|
365
|
+
if (name && semi >= 0 && semi < stop) frames.at(-1).push(name[1]);
|
|
366
|
+
i = semi >= 0 && semi < stop ? semi + 1 : stop;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
let header = "";
|
|
370
|
+
while (j < stop && /\s/.test(s[j])) j++;
|
|
371
|
+
if (s[j] === "(") {
|
|
372
|
+
const end = closeParen(s, j, stop);
|
|
373
|
+
if (end < 0) break;
|
|
374
|
+
header = s.slice(j + 1, end - 1);
|
|
375
|
+
j = end;
|
|
376
|
+
}
|
|
377
|
+
while (j < stop && /\s/.test(s[j])) j++;
|
|
378
|
+
if (s[j] === "{") {
|
|
379
|
+
frames.push(headerNames(kw, header));
|
|
380
|
+
i = j + 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
i = j;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (c === "}" && frames.length > 1) frames.pop();
|
|
388
|
+
i++;
|
|
389
|
+
}
|
|
390
|
+
return [...inputs, ...frames.flat()];
|
|
391
|
+
}
|
|
392
|
+
function exprContext(segment, s, offset) {
|
|
393
|
+
const pipe = /\|\s*([A-Za-z_$][\w$]*)?$/.exec(segment);
|
|
394
|
+
if (pipe && !/\|\|\s*[\w$]*$/.test(segment)) return {
|
|
395
|
+
kind: "pipe",
|
|
396
|
+
prefix: pipe[1] ?? ""
|
|
397
|
+
};
|
|
398
|
+
if (/\??\.\s*[\w$]*$/.test(segment)) return { kind: "other" };
|
|
399
|
+
return {
|
|
400
|
+
kind: "expr",
|
|
401
|
+
prefix: /([A-Za-z_$][\w$]*)?$/.exec(segment)[1] ?? "",
|
|
402
|
+
scope: scopeAt(s, offset)
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function cursorContext(s, offset) {
|
|
406
|
+
const before = s.slice(0, offset);
|
|
407
|
+
const open = before.lastIndexOf("{{");
|
|
408
|
+
if (open >= 0 && open > before.lastIndexOf("}}") && !before.startsWith("{{--", open)) return exprContext(before.slice(open + 2), s, offset);
|
|
409
|
+
const named = /@(layout|slot)\s*\(\s*['"]([^'"]*)$/.exec(before);
|
|
410
|
+
if (named) return {
|
|
411
|
+
kind: named[1] === "layout" ? "template-name" : "slot-name",
|
|
412
|
+
prefix: named[2]
|
|
413
|
+
};
|
|
414
|
+
const letExpr = /@let\s+[\w$]+\s*=\s*([^;\n]*)$/.exec(before);
|
|
415
|
+
if (letExpr) return exprContext(letExpr[1], s, offset);
|
|
416
|
+
const header = /@(?:else\s+if|if|for|switch|case)\s*\(([^]*)$/.exec(before);
|
|
417
|
+
if (header && closeParen(before, before.length - header[1].length - 1, before.length) < 0) return exprContext(header[1], s, offset);
|
|
418
|
+
const tag = /<([A-Za-z][\w:.-]*)((?:[^<>"']|"[^"]*"|'[^']*')*?)(["'][^"']*)?$/.exec(before);
|
|
419
|
+
if (tag && !before.endsWith(">")) {
|
|
420
|
+
const [, name, attrs = "", openValue] = tag;
|
|
421
|
+
if (openValue !== void 0) {
|
|
422
|
+
if (/\[[^\]]+\]\s*=\s*$/.test(attrs)) return exprContext(openValue.slice(1), s, offset);
|
|
423
|
+
return { kind: "other" };
|
|
424
|
+
}
|
|
425
|
+
if (!attrs) return {
|
|
426
|
+
kind: "tag",
|
|
427
|
+
prefix: name
|
|
428
|
+
};
|
|
429
|
+
if (name.includes("-")) {
|
|
430
|
+
const attr = /\s\[?([\w-]*)$/.exec(attrs);
|
|
431
|
+
if (attr) return {
|
|
432
|
+
kind: "attr",
|
|
433
|
+
tag: name.toLowerCase(),
|
|
434
|
+
prefix: attr[1]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return { kind: "other" };
|
|
438
|
+
}
|
|
439
|
+
const kw = /(?<![\w@.-])@([A-Za-z]*)$/.exec(before);
|
|
440
|
+
if (kw) return {
|
|
441
|
+
kind: "keyword",
|
|
442
|
+
prefix: kw[1]
|
|
443
|
+
};
|
|
444
|
+
return { kind: "other" };
|
|
445
|
+
}
|
|
446
|
+
/** The name under the cursor (pipes, tags, template names) and its range. */
|
|
447
|
+
function wordAt(s, offset) {
|
|
448
|
+
let start = offset;
|
|
449
|
+
let end = offset;
|
|
450
|
+
while (start > 0 && /[\w$/-]/.test(s[start - 1])) start--;
|
|
451
|
+
while (end < s.length && /[\w$/-]/.test(s[end])) end++;
|
|
452
|
+
return {
|
|
453
|
+
word: s.slice(start, end),
|
|
454
|
+
start,
|
|
455
|
+
end
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/plugin/completion.ts
|
|
460
|
+
const SNIPPET = 2;
|
|
461
|
+
const KEYWORD = 14;
|
|
462
|
+
const FUNCTION = 3;
|
|
463
|
+
const CLASS = 7;
|
|
464
|
+
const PROPERTY = 10;
|
|
465
|
+
const FILE = 17;
|
|
466
|
+
const VARIABLE = 6;
|
|
467
|
+
const KEYWORDS = {
|
|
468
|
+
if: "if (${1:condition}) {\n $0\n}",
|
|
469
|
+
"else if": "else if (${1:condition}) {\n $0\n}",
|
|
470
|
+
else: "else {\n $0\n}",
|
|
471
|
+
for: "for (${1:item} of ${2:items}) {\n $0\n}",
|
|
472
|
+
empty: "empty {\n $0\n}",
|
|
473
|
+
switch: "switch (${1:value}) {\n @case (${2:match}) {\n $0\n }\n}",
|
|
474
|
+
case: "case (${1:match}) {\n $0\n}",
|
|
475
|
+
default: "default {\n $0\n}",
|
|
476
|
+
let: "let ${1:name} = $2;",
|
|
477
|
+
input: "input ${1:name}",
|
|
478
|
+
layout: "layout('$1')",
|
|
479
|
+
slot: "slot('${1:name}') {\n $0\n}",
|
|
480
|
+
assets: "assets('$1')",
|
|
481
|
+
verbatim: "verbatim {\n $0\n}"
|
|
482
|
+
};
|
|
483
|
+
/** The template a component tag names, if any. */
|
|
484
|
+
function componentOf(ws, tag) {
|
|
485
|
+
const dir = ws.componentsDir();
|
|
486
|
+
return ws.names().find((n) => n.startsWith(`${dir}/`) && ws.view.componentTag(n, dir) === tag);
|
|
487
|
+
}
|
|
488
|
+
/** Slot names the layouts above `name` declare, nearest first. */
|
|
489
|
+
function layoutSlots(ws, name) {
|
|
490
|
+
const out = /* @__PURE__ */ new Set();
|
|
491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
492
|
+
let layout = name === void 0 ? void 0 : ws.astOf(name)?.layout;
|
|
493
|
+
while (typeof layout === "string" && !seen.has(layout)) {
|
|
494
|
+
seen.add(layout);
|
|
495
|
+
const ast = ws.astOf(layout);
|
|
496
|
+
if (!ast) break;
|
|
497
|
+
const stack = [ast.body];
|
|
498
|
+
while (stack.length) for (const n of stack.pop()) if (n.k === "slot-outlet") {
|
|
499
|
+
if (n.name) out.add(n.name);
|
|
500
|
+
stack.push(n.fallback);
|
|
501
|
+
} else if (n.k === "element") stack.push(n.children);
|
|
502
|
+
else if (n.k === "if") for (const b of n.branches) stack.push(b.body);
|
|
503
|
+
else if (n.k === "for") stack.push(n.body, n.empty ?? []);
|
|
504
|
+
else if (n.k === "switch") stack.push(...n.cases.map((c) => c.body), n.default ?? []);
|
|
505
|
+
else if (n.k === "slot-fill") stack.push(n.body);
|
|
506
|
+
layout = ast.layout;
|
|
507
|
+
}
|
|
508
|
+
return [...out];
|
|
509
|
+
}
|
|
510
|
+
function completions(ws, uri, text, offset) {
|
|
511
|
+
const c = cursorContext(text, offset);
|
|
512
|
+
switch (c.kind) {
|
|
513
|
+
case "keyword": return Object.entries(KEYWORDS).map(([label, insertText]) => ({
|
|
514
|
+
label,
|
|
515
|
+
kind: KEYWORD,
|
|
516
|
+
insertText,
|
|
517
|
+
insertTextFormat: SNIPPET
|
|
518
|
+
}));
|
|
519
|
+
case "pipe": return ws.pipeNames().map((label) => ({
|
|
520
|
+
label,
|
|
521
|
+
kind: FUNCTION
|
|
522
|
+
}));
|
|
523
|
+
case "tag": {
|
|
524
|
+
const dir = ws.componentsDir();
|
|
525
|
+
return ws.names().filter((n) => n.startsWith(`${dir}/`)).map((n) => {
|
|
526
|
+
const tag = ws.view.componentTag(n, dir);
|
|
527
|
+
const attrs = (ws.astOf(n)?.inputs ?? []).filter((i) => !i.default).map((i, k) => ` [${i.name}]="$${k + 1}"`).join("");
|
|
528
|
+
return {
|
|
529
|
+
label: tag,
|
|
530
|
+
kind: CLASS,
|
|
531
|
+
insertText: `${tag}${attrs} />`,
|
|
532
|
+
insertTextFormat: SNIPPET
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
case "attr": {
|
|
537
|
+
const name = componentOf(ws, c.tag);
|
|
538
|
+
if (!name) return [];
|
|
539
|
+
return [
|
|
540
|
+
...(ws.astOf(name)?.inputs ?? []).flatMap((i) => [{
|
|
541
|
+
label: `[${i.name}]`,
|
|
542
|
+
kind: PROPERTY,
|
|
543
|
+
insertText: `[${i.name}]="$1"`,
|
|
544
|
+
insertTextFormat: SNIPPET
|
|
545
|
+
}, {
|
|
546
|
+
label: i.name,
|
|
547
|
+
kind: PROPERTY,
|
|
548
|
+
insertText: `${i.name}="$1"`,
|
|
549
|
+
insertTextFormat: SNIPPET
|
|
550
|
+
}]),
|
|
551
|
+
{
|
|
552
|
+
label: "class",
|
|
553
|
+
kind: PROPERTY
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
label: "id",
|
|
557
|
+
kind: PROPERTY
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
case "template-name": {
|
|
562
|
+
const dir = ws.componentsDir();
|
|
563
|
+
return ws.names().filter((n) => !n.startsWith(`${dir}/`)).map((label) => ({
|
|
564
|
+
label,
|
|
565
|
+
kind: FILE
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
case "slot-name": return layoutSlots(ws, ws.nameOf(uri)).map((label) => ({
|
|
569
|
+
label,
|
|
570
|
+
kind: PROPERTY
|
|
571
|
+
}));
|
|
572
|
+
case "expr": return [...new Set(c.scope)].map((label) => ({
|
|
573
|
+
label,
|
|
574
|
+
kind: VARIABLE
|
|
575
|
+
}));
|
|
576
|
+
default: return [];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/plugin/hover.ts
|
|
581
|
+
/** Worded as in the guide's pipe table. */
|
|
582
|
+
const BUILTIN = {
|
|
583
|
+
uppercase: "`uppercase` — the text in upper case",
|
|
584
|
+
lowercase: "`lowercase` — the text in lower case",
|
|
585
|
+
titlecase: "`titlecase` — each word capitalised",
|
|
586
|
+
date: "`date:format:timeZone?` — `short`, `medium`, `long`, `full`, `shortDate`, … e.g. `date:'short'`",
|
|
587
|
+
number: "`number:digits` — `minInt.minFrac-maxFrac`, e.g. `number:'1.0-2'`",
|
|
588
|
+
currency: "`currency:code:digits?` — e.g. `currency:'KES'`",
|
|
589
|
+
percent: "`percent:digits?` — a ratio as a percentage",
|
|
590
|
+
slice: "`slice:start:end` — part of a list or string",
|
|
591
|
+
keys: "`keys` — an object’s keys",
|
|
592
|
+
entries: "`entries` — an object’s `[key, value]` pairs",
|
|
593
|
+
json: "`json` — the value as JSON",
|
|
594
|
+
url: "`url` — URL-encoded for a query string",
|
|
595
|
+
raw: "`raw` — the HTML unescaped; only for trusted markup"
|
|
596
|
+
};
|
|
597
|
+
function hover(ws, _uri, text, offset) {
|
|
598
|
+
const { word, start, end } = wordAt(text, offset);
|
|
599
|
+
if (!word) return null;
|
|
600
|
+
const c = cursorContext(text, end);
|
|
601
|
+
const md = (value) => ({
|
|
602
|
+
contents: {
|
|
603
|
+
kind: "markdown",
|
|
604
|
+
value
|
|
605
|
+
},
|
|
606
|
+
range: {
|
|
607
|
+
start: posOf(text, start),
|
|
608
|
+
end: posOf(text, end)
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
if (c.kind === "pipe") {
|
|
612
|
+
if (BUILTIN[word]) return md(BUILTIN[word]);
|
|
613
|
+
const decl = ws.pipeDeclaration(word);
|
|
614
|
+
return decl ? md(`\`\`\`ts\n${decl.signature}\n\`\`\``) : null;
|
|
615
|
+
}
|
|
616
|
+
if (c.kind === "tag") {
|
|
617
|
+
const name = componentOf(ws, word.toLowerCase());
|
|
618
|
+
const ast = name === void 0 ? void 0 : ws.astOf(name);
|
|
619
|
+
if (!ast) return null;
|
|
620
|
+
const lines = ast.inputs.map((i) => i.default ? `- \`${i.name}\` = \`${JSON.stringify(i.default.value)}\`` : `- \`${i.name}\` (required)`);
|
|
621
|
+
return md([`**<${word}>** — \`${name}.kick\``, ...lines].join("\n"));
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
function posOf(text, offset) {
|
|
626
|
+
const before = text.slice(0, offset).split("\n");
|
|
627
|
+
return {
|
|
628
|
+
line: before.length - 1,
|
|
629
|
+
character: before.at(-1).length
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/plugin/definition.ts
|
|
634
|
+
const TOP = {
|
|
635
|
+
start: {
|
|
636
|
+
line: 0,
|
|
637
|
+
character: 0
|
|
638
|
+
},
|
|
639
|
+
end: {
|
|
640
|
+
line: 0,
|
|
641
|
+
character: 0
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function definition(ws, _uri, text, offset) {
|
|
645
|
+
const { word, start, end } = wordAt(text, offset);
|
|
646
|
+
if (!word) return null;
|
|
647
|
+
const origin = {
|
|
648
|
+
start: posOf(text, start),
|
|
649
|
+
end: posOf(text, end)
|
|
650
|
+
};
|
|
651
|
+
const link = (targetUri, targetRange = TOP) => ({
|
|
652
|
+
originSelectionRange: origin,
|
|
653
|
+
targetUri,
|
|
654
|
+
targetRange,
|
|
655
|
+
targetSelectionRange: targetRange
|
|
656
|
+
});
|
|
657
|
+
const c = cursorContext(text, end);
|
|
658
|
+
if (c.kind === "tag") {
|
|
659
|
+
const name = componentOf(ws, word.toLowerCase());
|
|
660
|
+
return name === void 0 ? null : link(ws.uriOf(name));
|
|
661
|
+
}
|
|
662
|
+
if (c.kind === "template-name") return ws.astOf(word) ? link(ws.uriOf(word)) : null;
|
|
663
|
+
if (c.kind === "pipe") {
|
|
664
|
+
const decl = ws.pipeDeclaration(word);
|
|
665
|
+
if (!decl) return null;
|
|
666
|
+
const range = {
|
|
667
|
+
start: {
|
|
668
|
+
line: decl.line,
|
|
669
|
+
character: decl.column
|
|
670
|
+
},
|
|
671
|
+
end: {
|
|
672
|
+
line: decl.line,
|
|
673
|
+
character: decl.column + word.length
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
return link(URI.file(ws.pipesFile()).toString(), range);
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/plugin/diagnostics.ts
|
|
682
|
+
/** The `.kick` file a root virtual-code document belongs to. */
|
|
683
|
+
function sourceUri(context, uri) {
|
|
684
|
+
return context.decodeEmbeddedDocumentUri?.(URI.parse(uri))?.[0].toString() ?? uri;
|
|
685
|
+
}
|
|
686
|
+
function createDiagnosticsPlugin(ws) {
|
|
687
|
+
return {
|
|
688
|
+
name: "kick-diagnostics",
|
|
689
|
+
capabilities: { diagnosticProvider: {
|
|
690
|
+
interFileDependencies: true,
|
|
691
|
+
workspaceDiagnostics: false
|
|
692
|
+
} },
|
|
693
|
+
create(context) {
|
|
694
|
+
return { provideDiagnostics(document) {
|
|
695
|
+
if (document.languageId !== "kick") return void 0;
|
|
696
|
+
const uri = sourceUri(context, document.uri);
|
|
697
|
+
ws.sync(uri, document.getText());
|
|
698
|
+
return ws.diagnosticsFor(uri);
|
|
699
|
+
} };
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/plugin/format.ts
|
|
705
|
+
/** One whole-document edit, or none on a parse error or when nothing changes. */
|
|
706
|
+
function formatEdits(view, text, file, tabSize) {
|
|
707
|
+
const { output } = view.format(text, file, { indent: tabSize });
|
|
708
|
+
if (output === void 0 || output === text) return [];
|
|
709
|
+
return [{
|
|
710
|
+
range: {
|
|
711
|
+
start: {
|
|
712
|
+
line: 0,
|
|
713
|
+
character: 0
|
|
714
|
+
},
|
|
715
|
+
end: posOf(text, text.length)
|
|
716
|
+
},
|
|
717
|
+
newText: output
|
|
718
|
+
}];
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/plugin/index.ts
|
|
722
|
+
function createKickServicePlugin(ws) {
|
|
723
|
+
const diagnostics = createDiagnosticsPlugin(ws);
|
|
724
|
+
return {
|
|
725
|
+
name: "kick",
|
|
726
|
+
capabilities: {
|
|
727
|
+
...diagnostics.capabilities,
|
|
728
|
+
completionProvider: { triggerCharacters: [
|
|
729
|
+
"@",
|
|
730
|
+
"|",
|
|
731
|
+
"<",
|
|
732
|
+
"[",
|
|
733
|
+
"'",
|
|
734
|
+
"\""
|
|
735
|
+
] },
|
|
736
|
+
hoverProvider: true,
|
|
737
|
+
definitionProvider: true,
|
|
738
|
+
documentFormattingProvider: true
|
|
739
|
+
},
|
|
740
|
+
create(context) {
|
|
741
|
+
/** The source file's uri and text, with the workspace synced to the buffer. */
|
|
742
|
+
const open = (document) => {
|
|
743
|
+
if (document.languageId !== "kick") return void 0;
|
|
744
|
+
const uri = sourceUri(context, document.uri);
|
|
745
|
+
const text = document.getText();
|
|
746
|
+
ws.sync(uri, text);
|
|
747
|
+
return {
|
|
748
|
+
uri,
|
|
749
|
+
text
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
return {
|
|
753
|
+
...diagnostics.create(context),
|
|
754
|
+
provideCompletionItems(document, position) {
|
|
755
|
+
const d = open(document);
|
|
756
|
+
if (!d) return void 0;
|
|
757
|
+
const items = completions(ws, d.uri, d.text, document.offsetAt(position));
|
|
758
|
+
return items.length ? {
|
|
759
|
+
isIncomplete: false,
|
|
760
|
+
items
|
|
761
|
+
} : void 0;
|
|
762
|
+
},
|
|
763
|
+
provideHover(document, position) {
|
|
764
|
+
const d = open(document);
|
|
765
|
+
return d ? hover(ws, d.uri, d.text, document.offsetAt(position)) ?? void 0 : void 0;
|
|
766
|
+
},
|
|
767
|
+
provideDefinition(document, position) {
|
|
768
|
+
const d = open(document);
|
|
769
|
+
const link = d && definition(ws, d.uri, d.text, document.offsetAt(position));
|
|
770
|
+
return link ? [link] : void 0;
|
|
771
|
+
},
|
|
772
|
+
provideDocumentFormattingEdits(document, _range, options) {
|
|
773
|
+
const d = open(document);
|
|
774
|
+
if (!d) return void 0;
|
|
775
|
+
return formatEdits(ws.view, d.text, `${ws.nameOf(d.uri) ?? "template"}.kick`, options.tabSize);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/view-module.ts
|
|
783
|
+
/**
|
|
784
|
+
* Which `@forinda/kickjs-view` the server runs: the project's own, so the
|
|
785
|
+
* editor parses, checks and formats exactly as the app's build does, or the
|
|
786
|
+
* copy bundled with the server when the project has none (or one too old to
|
|
787
|
+
* have what the editor calls).
|
|
788
|
+
*/
|
|
789
|
+
const BUNDLED_VERSION = "0.1.0";
|
|
790
|
+
const REQUIRED = [
|
|
791
|
+
"parse",
|
|
792
|
+
"check",
|
|
793
|
+
"format",
|
|
794
|
+
"readPipeExports",
|
|
795
|
+
"templateName",
|
|
796
|
+
"componentTag",
|
|
797
|
+
"BUILTIN_PIPES"
|
|
798
|
+
];
|
|
799
|
+
let warned = false;
|
|
800
|
+
/**
|
|
801
|
+
* The project's `@forinda/kickjs-view` entry and version. Its exports map
|
|
802
|
+
* only has `import`, which `require.resolve` can't see — so find the
|
|
803
|
+
* package folder and read the entry from its exports.
|
|
804
|
+
*/
|
|
805
|
+
function findProjectView(root) {
|
|
806
|
+
for (let dir = root;; dir = dirname(dir)) {
|
|
807
|
+
const pkgDir = join(dir, "node_modules/@forinda/kickjs-view");
|
|
808
|
+
let pkg;
|
|
809
|
+
try {
|
|
810
|
+
pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8"));
|
|
811
|
+
} catch {
|
|
812
|
+
if (dir === dirname(dir)) return void 0;
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const dot = typeof pkg.exports === "string" ? pkg.exports : pkg.exports?.["."];
|
|
816
|
+
const rel = typeof dot === "string" ? dot : dot?.node ?? dot?.import ?? dot?.default ?? pkg.main;
|
|
817
|
+
const entry = resolve(pkgDir, typeof rel === "string" ? rel : "index.js");
|
|
818
|
+
if (relative(pkgDir, entry).startsWith("..") || isAbsolute(relative(pkgDir, entry))) return void 0;
|
|
819
|
+
return {
|
|
820
|
+
entry,
|
|
821
|
+
version: pkg.version
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function loadViewModule(root, warn, trusted) {
|
|
826
|
+
if (!trusted) return {
|
|
827
|
+
view: bundled,
|
|
828
|
+
source: "bundled",
|
|
829
|
+
version: BUNDLED_VERSION
|
|
830
|
+
};
|
|
831
|
+
const found = findProjectView(root);
|
|
832
|
+
let reason = "the project has no @forinda/kickjs-view";
|
|
833
|
+
if (found) {
|
|
834
|
+
reason = `the project's @forinda/kickjs-view ${found.version}`;
|
|
835
|
+
try {
|
|
836
|
+
const mod = await import(pathToFileURL(found.entry).href);
|
|
837
|
+
const missing = REQUIRED.filter((k) => !(k in mod));
|
|
838
|
+
if (!missing.length) return {
|
|
839
|
+
view: mod,
|
|
840
|
+
source: "project",
|
|
841
|
+
version: found.version
|
|
842
|
+
};
|
|
843
|
+
reason += ` lacks ${missing.join(", ")}`;
|
|
844
|
+
} catch (err) {
|
|
845
|
+
reason += ` failed to load: ${err.message}`;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (!warned) {
|
|
849
|
+
warned = true;
|
|
850
|
+
warn(`kick views: ${reason} — using the bundled ${BUNDLED_VERSION}`);
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
view: bundled,
|
|
854
|
+
source: "bundled",
|
|
855
|
+
version: BUNDLED_VERSION
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/pipes.ts
|
|
860
|
+
const DECL = /^export\s+(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]*)?=\s*(?:\(([^)]*)\)|([A-Za-z_$][\w$]*)\s*=>)|function\s+([A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\(([^)]*)\))/d;
|
|
861
|
+
function pipeDeclarations(source) {
|
|
862
|
+
const out = /* @__PURE__ */ new Map();
|
|
863
|
+
source.split("\n").forEach((text, line) => {
|
|
864
|
+
const m = DECL.exec(text);
|
|
865
|
+
if (!m) return;
|
|
866
|
+
const group = m[1] === void 0 ? 4 : 1;
|
|
867
|
+
const name = m[group];
|
|
868
|
+
const params = (m[2] ?? m[3] ?? m[5] ?? "").replace(/\s+/g, " ").trim();
|
|
869
|
+
out.set(name, {
|
|
870
|
+
line,
|
|
871
|
+
column: m.indices[group][0],
|
|
872
|
+
signature: `${name}(${params})`
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
//#endregion
|
|
878
|
+
//#region src/positions.ts
|
|
879
|
+
const toPosition = (pos) => ({
|
|
880
|
+
line: pos.line - 1,
|
|
881
|
+
character: pos.column - 1
|
|
882
|
+
});
|
|
883
|
+
const toRange = (span) => ({
|
|
884
|
+
start: toPosition(span.start),
|
|
885
|
+
end: toPosition(span.end)
|
|
886
|
+
});
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/workspace.ts
|
|
889
|
+
/**
|
|
890
|
+
* The editor's view of an app's templates: every `.kick` file under the
|
|
891
|
+
* views folder (an open buffer wins over the file on disk) and the pipe names
|
|
892
|
+
* from `pipes.ts`. Diagnostics come from the same `parse` + `check` the
|
|
893
|
+
* runtime uses, cached per template and invalidated along the graph of
|
|
894
|
+
* which template uses which component or layout.
|
|
895
|
+
*/
|
|
896
|
+
var Workspace = class {
|
|
897
|
+
opts;
|
|
898
|
+
entries = /* @__PURE__ */ new Map();
|
|
899
|
+
cache = /* @__PURE__ */ new Map();
|
|
900
|
+
pipes = [];
|
|
901
|
+
pipeDecls = /* @__PURE__ */ new Map();
|
|
902
|
+
constructor(opts) {
|
|
903
|
+
this.opts = opts;
|
|
904
|
+
}
|
|
905
|
+
get view() {
|
|
906
|
+
return this.opts.view;
|
|
907
|
+
}
|
|
908
|
+
async load() {
|
|
909
|
+
await this.loadPipes();
|
|
910
|
+
for (const path of await this.opts.list(this.opts.viewsDir)) {
|
|
911
|
+
const name = this.nameOfPath(path);
|
|
912
|
+
if (name) this.update(name, { disk: await this.opts.read(path) });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
nameOfPath(path) {
|
|
916
|
+
const rel = relative(this.opts.viewsDir, path).split(sep).join(posix.sep);
|
|
917
|
+
if (rel.startsWith("..") || !rel.endsWith(".kick")) return void 0;
|
|
918
|
+
return this.opts.view.templateName(rel.slice(0, -5));
|
|
919
|
+
}
|
|
920
|
+
nameOf(uri) {
|
|
921
|
+
return this.nameOfPath(URI.parse(uri).fsPath);
|
|
922
|
+
}
|
|
923
|
+
uriOf(name) {
|
|
924
|
+
return URI.file(`${this.opts.viewsDir}/${name}.kick`).toString();
|
|
925
|
+
}
|
|
926
|
+
componentsDir() {
|
|
927
|
+
return this.opts.componentsDir;
|
|
928
|
+
}
|
|
929
|
+
pathOf(name) {
|
|
930
|
+
return `${this.opts.viewsDir}/${name}.kick`;
|
|
931
|
+
}
|
|
932
|
+
names() {
|
|
933
|
+
return [...this.entries.keys()];
|
|
934
|
+
}
|
|
935
|
+
textOf(name) {
|
|
936
|
+
const e = this.entries.get(name);
|
|
937
|
+
return e?.buffer ?? e?.disk;
|
|
938
|
+
}
|
|
939
|
+
astOf(name) {
|
|
940
|
+
return this.entries.get(name)?.ast;
|
|
941
|
+
}
|
|
942
|
+
/** An editor buffer for `uri`; `undefined` drops it and the file on disk applies again. */
|
|
943
|
+
setBuffer(uri, text) {
|
|
944
|
+
const name = this.nameOf(uri);
|
|
945
|
+
if (!name) return;
|
|
946
|
+
const e = this.entries.get(name);
|
|
947
|
+
if (text === void 0 && e?.disk === void 0) {
|
|
948
|
+
this.remove(name);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
this.update(name, {
|
|
952
|
+
disk: e?.disk,
|
|
953
|
+
buffer: text
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/** `setBuffer`, skipped when the buffer is already `text`. */
|
|
957
|
+
sync(uri, text) {
|
|
958
|
+
if (this.textOf(this.nameOf(uri) ?? "") !== text) this.setBuffer(uri, text);
|
|
959
|
+
}
|
|
960
|
+
async fileChanged(path) {
|
|
961
|
+
if (path === this.opts.pipesFile) {
|
|
962
|
+
await this.loadPipes();
|
|
963
|
+
this.cache.clear();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const name = this.nameOfPath(path);
|
|
967
|
+
if (name) this.update(name, {
|
|
968
|
+
disk: await this.opts.read(path),
|
|
969
|
+
buffer: this.entries.get(name)?.buffer
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async fileDeleted(path) {
|
|
973
|
+
if (path === this.opts.pipesFile) {
|
|
974
|
+
this.pipes = [];
|
|
975
|
+
this.pipeDecls = /* @__PURE__ */ new Map();
|
|
976
|
+
this.cache.clear();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const name = this.nameOfPath(path);
|
|
980
|
+
if (!name) return;
|
|
981
|
+
const buffer = this.entries.get(name)?.buffer;
|
|
982
|
+
if (buffer !== void 0) this.update(name, { buffer });
|
|
983
|
+
else this.remove(name);
|
|
984
|
+
}
|
|
985
|
+
async loadPipes() {
|
|
986
|
+
const source = await this.opts.read(this.opts.pipesFile);
|
|
987
|
+
this.pipes = source === void 0 ? [] : this.opts.view.readPipeExports(source, this.opts.pipesFile).names;
|
|
988
|
+
this.pipeDecls = source === void 0 ? /* @__PURE__ */ new Map() : pipeDeclarations(source);
|
|
989
|
+
}
|
|
990
|
+
update(name, src) {
|
|
991
|
+
const text = src.buffer ?? src.disk ?? "";
|
|
992
|
+
const parsed = this.opts.view.parse(text, `${name}.kick`);
|
|
993
|
+
const tags = /* @__PURE__ */ new Set();
|
|
994
|
+
const layouts = /* @__PURE__ */ new Set();
|
|
995
|
+
if (typeof parsed.ast.layout === "string") layouts.add(parsed.ast.layout);
|
|
996
|
+
const stack = [parsed.ast.body];
|
|
997
|
+
while (stack.length) for (const n of stack.pop()) if (n.k === "element") {
|
|
998
|
+
if (!n.foreign && n.tag.includes("-")) tags.add(n.tag.toLowerCase());
|
|
999
|
+
stack.push(n.children);
|
|
1000
|
+
} else if (n.k === "if") for (const b of n.branches) stack.push(b.body);
|
|
1001
|
+
else if (n.k === "for") stack.push(n.body, n.empty ?? []);
|
|
1002
|
+
else if (n.k === "switch") stack.push(...n.cases.map((c) => c.body), n.default ?? []);
|
|
1003
|
+
else if (n.k === "slot-outlet") stack.push(n.fallback);
|
|
1004
|
+
else if (n.k === "slot-fill") stack.push(n.body);
|
|
1005
|
+
const isNew = !this.entries.has(name);
|
|
1006
|
+
this.entries.set(name, {
|
|
1007
|
+
...src,
|
|
1008
|
+
ast: parsed.ast,
|
|
1009
|
+
parseDiagnostics: parsed.diagnostics,
|
|
1010
|
+
tags,
|
|
1011
|
+
layouts
|
|
1012
|
+
});
|
|
1013
|
+
this.invalidate(name, isNew);
|
|
1014
|
+
}
|
|
1015
|
+
remove(name) {
|
|
1016
|
+
this.entries.delete(name);
|
|
1017
|
+
this.invalidate(name, true);
|
|
1018
|
+
}
|
|
1019
|
+
/** Drop cached diagnostics for `name` and every template that uses it, transitively. */
|
|
1020
|
+
invalidate(name, membershipChanged) {
|
|
1021
|
+
const todo = [name];
|
|
1022
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1023
|
+
while (todo.length) {
|
|
1024
|
+
const n = todo.pop();
|
|
1025
|
+
if (seen.has(n)) continue;
|
|
1026
|
+
seen.add(n);
|
|
1027
|
+
this.cache.delete(n);
|
|
1028
|
+
for (const d of this.dependentsOf(n)) todo.push(d);
|
|
1029
|
+
}
|
|
1030
|
+
if (membershipChanged && name.startsWith(`${this.opts.componentsDir}/`)) {
|
|
1031
|
+
const tag = this.opts.view.componentTag(name, this.opts.componentsDir);
|
|
1032
|
+
for (const [n, e] of this.entries) if (e.tags.has(tag)) this.cache.delete(n);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/** Templates that use `name` as a component or a layout — a layout even after it's gone. */
|
|
1036
|
+
dependentsOf(name) {
|
|
1037
|
+
const out = /* @__PURE__ */ new Set();
|
|
1038
|
+
const tag = name.startsWith(`${this.opts.componentsDir}/`) ? this.opts.view.componentTag(name, this.opts.componentsDir) : void 0;
|
|
1039
|
+
for (const [n, e] of this.entries) {
|
|
1040
|
+
if (n === name) continue;
|
|
1041
|
+
if (tag && e.tags.has(tag) && this.entries.has(name) || e.layouts.has(name)) out.add(n);
|
|
1042
|
+
}
|
|
1043
|
+
return out;
|
|
1044
|
+
}
|
|
1045
|
+
pipeNames() {
|
|
1046
|
+
return [...this.opts.view.BUILTIN_PIPES, ...this.pipes];
|
|
1047
|
+
}
|
|
1048
|
+
/** Where an app pipe is declared in `pipes.ts`, and its parameters. */
|
|
1049
|
+
pipeDeclaration(name) {
|
|
1050
|
+
return this.pipeDecls.get(name);
|
|
1051
|
+
}
|
|
1052
|
+
pipesFile() {
|
|
1053
|
+
return this.opts.pipesFile;
|
|
1054
|
+
}
|
|
1055
|
+
project() {
|
|
1056
|
+
return {
|
|
1057
|
+
templates: new Map([...this.entries].map(([n, e]) => [n, e.ast])),
|
|
1058
|
+
pipes: new Set(this.pipeNames()),
|
|
1059
|
+
componentsDir: this.opts.componentsDir
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/** Parse errors, else everything `check` reports — as LSP diagnostics. */
|
|
1063
|
+
diagnosticsFor(uri) {
|
|
1064
|
+
const name = this.nameOf(uri);
|
|
1065
|
+
const e = name === void 0 ? void 0 : this.entries.get(name);
|
|
1066
|
+
if (!name || !e) return [];
|
|
1067
|
+
const cached = this.cache.get(name);
|
|
1068
|
+
if (cached) return cached;
|
|
1069
|
+
const out = (e.parseDiagnostics.some((d) => d.severity === "error") ? e.parseDiagnostics : [...e.parseDiagnostics, ...this.opts.view.check(name, this.project()).diagnostics]).map((d) => ({
|
|
1070
|
+
range: toRange(d.span),
|
|
1071
|
+
severity: d.severity === "error" ? 1 : 2,
|
|
1072
|
+
source: "kick",
|
|
1073
|
+
message: d.message
|
|
1074
|
+
}));
|
|
1075
|
+
this.cache.set(name, out);
|
|
1076
|
+
return out;
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
//#endregion
|
|
1080
|
+
//#region src/server.ts
|
|
1081
|
+
/**
|
|
1082
|
+
* The language server: Volar's server with the `.kick` language plugin and
|
|
1083
|
+
* the HTML, CSS, Emmet and kick service plugins, over the project's own
|
|
1084
|
+
* `@forinda/kickjs-view` when it has one.
|
|
1085
|
+
*/
|
|
1086
|
+
/** Every `.kick` file under `dir`; none when it doesn't exist. */
|
|
1087
|
+
async function listKick(dir) {
|
|
1088
|
+
try {
|
|
1089
|
+
return (await readdir(dir, { recursive: true })).filter((n) => n.endsWith(".kick")).map((n) => join(dir, n));
|
|
1090
|
+
} catch {
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/** What the server watches: every template, and the pipes file wherever it's configured. */
|
|
1095
|
+
function watchPatterns(root, pipesFile) {
|
|
1096
|
+
const patterns = ["**/*.kick", "**/pipes.{ts,js}"];
|
|
1097
|
+
if (!/^pipes\.(ts|js)$/.test(basename(pipesFile))) patterns.push(relative(root, pipesFile).split(sep).join("/"));
|
|
1098
|
+
return patterns;
|
|
1099
|
+
}
|
|
1100
|
+
function startServer() {
|
|
1101
|
+
const connection = createConnection();
|
|
1102
|
+
const server = createServer(connection);
|
|
1103
|
+
let ws;
|
|
1104
|
+
let watched = [];
|
|
1105
|
+
connection.listen();
|
|
1106
|
+
connection.onInitialize(async (params) => {
|
|
1107
|
+
const folder = params.workspaceFolders?.[0]?.uri ?? params.rootUri;
|
|
1108
|
+
const root = folder ? URI.parse(folder).fsPath : process.cwd();
|
|
1109
|
+
const opts = params.initializationOptions ?? {};
|
|
1110
|
+
const at = (p) => isAbsolute(p) ? p : resolve(root, p);
|
|
1111
|
+
const viewsDir = at(opts.viewsDir ?? "src/views");
|
|
1112
|
+
const hasViews = existsSync(viewsDir);
|
|
1113
|
+
const { view } = await loadViewModule(root, (m) => hasViews && connection.console.warn(m), opts.trusted === true);
|
|
1114
|
+
const pipesFile = at(opts.pipesFile ?? join(viewsDir, "pipes.ts"));
|
|
1115
|
+
watched = watchPatterns(root, pipesFile);
|
|
1116
|
+
ws = new Workspace({
|
|
1117
|
+
root,
|
|
1118
|
+
viewsDir,
|
|
1119
|
+
componentsDir: opts.componentsDir ?? "components",
|
|
1120
|
+
pipesFile,
|
|
1121
|
+
view,
|
|
1122
|
+
read: (p) => readFile(p, "utf8").catch(() => void 0),
|
|
1123
|
+
list: listKick
|
|
1124
|
+
});
|
|
1125
|
+
await ws.load();
|
|
1126
|
+
return server.initialize(params, createSimpleProject([kickLanguagePlugin]), [
|
|
1127
|
+
create$2(),
|
|
1128
|
+
create(),
|
|
1129
|
+
create$1(),
|
|
1130
|
+
createKickServicePlugin(ws)
|
|
1131
|
+
]);
|
|
1132
|
+
});
|
|
1133
|
+
connection.onInitialized(() => {
|
|
1134
|
+
server.initialized();
|
|
1135
|
+
server.fileWatcher.watchFiles(watched);
|
|
1136
|
+
});
|
|
1137
|
+
server.fileWatcher.onDidChangeWatchedFiles(async ({ changes }) => {
|
|
1138
|
+
if (!ws) return;
|
|
1139
|
+
for (const c of changes) {
|
|
1140
|
+
const path = URI.parse(c.uri).fsPath;
|
|
1141
|
+
if (c.type === 3) await ws.fileDeleted(path);
|
|
1142
|
+
else await ws.fileChanged(path);
|
|
1143
|
+
}
|
|
1144
|
+
await server.languageFeatures.requestRefresh(false);
|
|
1145
|
+
});
|
|
1146
|
+
server.documents.onDidClose(({ document }) => ws?.setBuffer(document.uri, void 0));
|
|
1147
|
+
connection.onRequest("kick/templateNames", () => {
|
|
1148
|
+
if (!ws) return [];
|
|
1149
|
+
const dir = `${ws.componentsDir()}/`;
|
|
1150
|
+
return ws.names().filter((n) => !n.startsWith(dir));
|
|
1151
|
+
});
|
|
1152
|
+
connection.onShutdown(() => server.shutdown());
|
|
1153
|
+
}
|
|
1154
|
+
//#endregion
|
|
1155
|
+
export { KickVirtualCode as a, createKickServicePlugin as i, Workspace as n, kickLanguagePlugin as o, loadViewModule as r, startServer as t };
|
|
1156
|
+
|
|
1157
|
+
//# sourceMappingURL=server-3edMz3HM.js.map
|