@forinda/kickjs-view-language-server 0.1.2 → 0.3.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/README.md +6 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/main.js +2 -2
- package/dist/{server-CfzShU0v.js → server-Biz_hJF3.js} +746 -31
- package/dist/server-Biz_hJF3.js.map +1 -0
- package/package.json +2 -2
- package/dist/server-CfzShU0v.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-view-language-server v0.
|
|
2
|
+
* @forinda/kickjs-view-language-server v0.3.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -32,7 +32,7 @@ const EMBEDDED = {
|
|
|
32
32
|
...ALL,
|
|
33
33
|
format: false
|
|
34
34
|
};
|
|
35
|
-
const KEYWORD$1 = /^@(else\s+if|if|for|switch|case|slot|layout|assets|verbatim|let|input|else|empty|default)(?![\w$-])/;
|
|
35
|
+
const KEYWORD$1 = /^@(else\s+if|if|for|switch|case|slot|error|layout|assets|verbatim|let|input|else|empty|default)(?![\w$-])/;
|
|
36
36
|
const snapshotOf = (text) => ({
|
|
37
37
|
getText: (start, end) => text.slice(start, end),
|
|
38
38
|
getLength: () => text.length,
|
|
@@ -53,6 +53,48 @@ function closeParen$1(s, i) {
|
|
|
53
53
|
}
|
|
54
54
|
return s.length;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Where an `@let` value from `from` ends: its `;` outside quotes, as the
|
|
58
|
+
* parser reads it; the line's end while it's unterminated, so typing one
|
|
59
|
+
* doesn't swallow the lines below.
|
|
60
|
+
*/
|
|
61
|
+
function letEnd(s, from) {
|
|
62
|
+
let quote = "";
|
|
63
|
+
for (let j = from; j < s.length; j++) {
|
|
64
|
+
const c = s[j];
|
|
65
|
+
if (c === "\n") return {
|
|
66
|
+
at: j,
|
|
67
|
+
closed: false
|
|
68
|
+
};
|
|
69
|
+
if (quote) {
|
|
70
|
+
if (c === "\\") j++;
|
|
71
|
+
else if (c === quote) quote = "";
|
|
72
|
+
} else if (c === "'" || c === "\"") quote = c;
|
|
73
|
+
else if (c === ";") return {
|
|
74
|
+
at: j,
|
|
75
|
+
closed: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
at: s.length,
|
|
80
|
+
closed: false
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** The end of `@verbatim { … }` from `brace` (at `{`): after its `}`, quoted braces skipped as the parser does. */
|
|
84
|
+
function verbatimEnd(s, brace) {
|
|
85
|
+
let depth = 0;
|
|
86
|
+
let quote = "";
|
|
87
|
+
for (let k = brace; k < s.length; k++) {
|
|
88
|
+
const c = s[k];
|
|
89
|
+
if (quote) {
|
|
90
|
+
if (c === "\\") k++;
|
|
91
|
+
else if (c === quote) quote = "";
|
|
92
|
+
} else if (c === "'" || c === "\"" || c === "`") quote = c;
|
|
93
|
+
else if (c === "{") depth++;
|
|
94
|
+
else if (c === "}" && --depth === 0) return k + 1;
|
|
95
|
+
}
|
|
96
|
+
return s.length;
|
|
97
|
+
}
|
|
56
98
|
/** The end of a `{{ … }}` starting at `i`; the text's end when unclosed. */
|
|
57
99
|
function closeInterp(s, i) {
|
|
58
100
|
let quote = "";
|
|
@@ -108,9 +150,9 @@ function scan(s) {
|
|
|
108
150
|
const kw = m[1].replace(/\s+/g, " ");
|
|
109
151
|
let j = i + m[0].length;
|
|
110
152
|
if (kw === "let" || kw === "input") {
|
|
111
|
-
const semi = kw === "let" ? s.indexOf(";", j) : -1;
|
|
112
153
|
const eol = s.indexOf("\n", j);
|
|
113
|
-
const
|
|
154
|
+
const end = kw === "let" ? letEnd(s, j) : void 0;
|
|
155
|
+
const stop = end ? end.at + (end.closed ? 1 : 0) : eol < 0 ? s.length : eol;
|
|
114
156
|
out.blank.push([i, stop]);
|
|
115
157
|
i = stop;
|
|
116
158
|
continue;
|
|
@@ -118,11 +160,7 @@ function scan(s) {
|
|
|
118
160
|
if (kw === "verbatim") {
|
|
119
161
|
const b = skipWs(j);
|
|
120
162
|
if (s[b] === "{") {
|
|
121
|
-
|
|
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);
|
|
163
|
+
const stop = verbatimEnd(s, b);
|
|
126
164
|
out.blank.push([i, stop]);
|
|
127
165
|
i = stop;
|
|
128
166
|
continue;
|
|
@@ -205,7 +243,10 @@ function tag(s, i, out) {
|
|
|
205
243
|
k = e - 1;
|
|
206
244
|
}
|
|
207
245
|
if (name === "style") out.styles.push([j, end]);
|
|
208
|
-
else if (type !== "application/json" && type !== "application/ld+json") out.scripts.push(
|
|
246
|
+
else if (type !== "application/json" && type !== "application/ld+json") out.scripts.push({
|
|
247
|
+
range: [j, end],
|
|
248
|
+
ts: /\sclient(?=[\s/>=]|$)/i.test(s.slice(i, j))
|
|
249
|
+
});
|
|
209
250
|
return end;
|
|
210
251
|
}
|
|
211
252
|
function embedded(id, languageId, text, [from, to]) {
|
|
@@ -273,16 +314,42 @@ var KickVirtualCode = class {
|
|
|
273
314
|
mappings: htmlMappings
|
|
274
315
|
},
|
|
275
316
|
...styles.map((r, n) => embedded(`css_${n}`, "css", text, r)),
|
|
276
|
-
...scripts.map((
|
|
317
|
+
...scripts.map((c, n) => embedded(`js_${n}`, c.ts ? "typescript" : "javascript", text, c.range))
|
|
277
318
|
];
|
|
278
319
|
}
|
|
279
320
|
};
|
|
321
|
+
/** TS / JS: a code only the kick service plugin reads, for `ctx.render('…')` names. */
|
|
322
|
+
const SCRIPT_IDS = /* @__PURE__ */ new Set([
|
|
323
|
+
"typescript",
|
|
324
|
+
"typescriptreact",
|
|
325
|
+
"javascript",
|
|
326
|
+
"javascriptreact"
|
|
327
|
+
]);
|
|
328
|
+
const SCRIPT_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
329
|
+
var RenderCallsCode = class {
|
|
330
|
+
id = "root";
|
|
331
|
+
languageId = "kick-render";
|
|
332
|
+
snapshot;
|
|
333
|
+
mappings;
|
|
334
|
+
embeddedCodes = [];
|
|
335
|
+
constructor(text) {
|
|
336
|
+
this.snapshot = snapshotOf(text);
|
|
337
|
+
this.mappings = [{
|
|
338
|
+
sourceOffsets: [0],
|
|
339
|
+
generatedOffsets: [0],
|
|
340
|
+
lengths: [text.length],
|
|
341
|
+
data: EMBEDDED
|
|
342
|
+
}];
|
|
343
|
+
}
|
|
344
|
+
};
|
|
280
345
|
const kickLanguagePlugin = {
|
|
281
346
|
getLanguageId(uri) {
|
|
282
347
|
return uri.path.endsWith(".kick") ? "kick" : void 0;
|
|
283
348
|
},
|
|
284
349
|
createVirtualCode(uri, languageId, snapshot) {
|
|
285
|
-
|
|
350
|
+
const text = snapshot.getText(0, snapshot.getLength());
|
|
351
|
+
if (languageId === "kick" || uri.path.endsWith(".kick")) return new KickVirtualCode(text);
|
|
352
|
+
if (SCRIPT_IDS.has(languageId) || SCRIPT_FILE.test(uri.path)) return new RenderCallsCode(text);
|
|
286
353
|
}
|
|
287
354
|
};
|
|
288
355
|
//#endregion
|
|
@@ -295,7 +362,7 @@ const LOOP_VARS = [
|
|
|
295
362
|
"$odd",
|
|
296
363
|
"$count"
|
|
297
364
|
];
|
|
298
|
-
const IDENT = /[A-Za-z_$][\w$]*/;
|
|
365
|
+
const IDENT$1 = /[A-Za-z_$][\w$]*/;
|
|
299
366
|
/** The end of `(`…`)` from `i` (at `(`), or -1 when it doesn't close before `stop`. */
|
|
300
367
|
function closeParen(s, i, stop) {
|
|
301
368
|
let depth = 0;
|
|
@@ -323,6 +390,7 @@ function headerNames(kw, header) {
|
|
|
323
390
|
}
|
|
324
391
|
return [...names.filter(Boolean), ...LOOP_VARS];
|
|
325
392
|
}
|
|
393
|
+
if (kw === "error") return ["$message"];
|
|
326
394
|
if (kw === "if" || kw === "else if") {
|
|
327
395
|
const as = /;\s*as\s+([\w$]+)/.exec(header);
|
|
328
396
|
return as ? [as[1]] : [];
|
|
@@ -349,12 +417,12 @@ function scopeAt(s, stop) {
|
|
|
349
417
|
continue;
|
|
350
418
|
}
|
|
351
419
|
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));
|
|
420
|
+
const m = /^@(else\s+if|for|if|switch|case|slot|error|else|empty|default|let|input)(?![\w$-])/.exec(s.slice(i, i + 16));
|
|
353
421
|
if (m) {
|
|
354
422
|
const kw = m[1].replace(/\s+/g, " ");
|
|
355
423
|
let j = i + m[0].length;
|
|
356
424
|
if (kw === "input") {
|
|
357
|
-
const name = IDENT.exec(s.slice(j, j + 64));
|
|
425
|
+
const name = IDENT$1.exec(s.slice(j, j + 64));
|
|
358
426
|
if (name) inputs.push(name[0]);
|
|
359
427
|
i = j;
|
|
360
428
|
continue;
|
|
@@ -462,8 +530,14 @@ const KEYWORD = 14;
|
|
|
462
530
|
const FUNCTION = 3;
|
|
463
531
|
const CLASS = 7;
|
|
464
532
|
const PROPERTY = 10;
|
|
465
|
-
const FILE = 17;
|
|
533
|
+
const FILE$1 = 17;
|
|
466
534
|
const VARIABLE = 6;
|
|
535
|
+
/** Request-wide names every template has (see `@forinda/kickjs-view`'s form state). */
|
|
536
|
+
const FORM_STATE = [
|
|
537
|
+
"$errors",
|
|
538
|
+
"$old",
|
|
539
|
+
"$flash"
|
|
540
|
+
];
|
|
467
541
|
const KEYWORDS = {
|
|
468
542
|
if: "if (${1:condition}) {\n $0\n}",
|
|
469
543
|
"else if": "else if (${1:condition}) {\n $0\n}",
|
|
@@ -477,6 +551,7 @@ const KEYWORDS = {
|
|
|
477
551
|
input: "input ${1:name}",
|
|
478
552
|
layout: "layout('$1')",
|
|
479
553
|
slot: "slot('${1:name}') {\n $0\n}",
|
|
554
|
+
error: "error('${1:field}') {\n $0\n}",
|
|
480
555
|
assets: "assets('$1')",
|
|
481
556
|
verbatim: "verbatim {\n $0\n}"
|
|
482
557
|
};
|
|
@@ -489,11 +564,19 @@ function componentOf(ws, tag) {
|
|
|
489
564
|
function layoutSlots(ws, name) {
|
|
490
565
|
const out = /* @__PURE__ */ new Set();
|
|
491
566
|
const seen = /* @__PURE__ */ new Set();
|
|
492
|
-
|
|
493
|
-
|
|
567
|
+
const chain = [];
|
|
568
|
+
let layout = name === void 0 ? void 0 : ws.layoutOf(name);
|
|
569
|
+
while (layout !== void 0 && !seen.has(layout)) {
|
|
494
570
|
seen.add(layout);
|
|
495
|
-
|
|
496
|
-
|
|
571
|
+
chain.push(layout);
|
|
572
|
+
layout = ws.layoutOf(layout);
|
|
573
|
+
}
|
|
574
|
+
const base = ws.baseLayout();
|
|
575
|
+
const page = name === void 0 ? void 0 : ws.astOf(name);
|
|
576
|
+
if (base !== void 0 && name !== void 0 && page?.layout !== null && !name.startsWith(`${ws.componentsDir()}/`) && name !== base && !seen.has(base)) chain.push(base);
|
|
577
|
+
for (const l of chain) {
|
|
578
|
+
const ast = ws.astOf(l);
|
|
579
|
+
if (!ast) continue;
|
|
497
580
|
const stack = [ast.body];
|
|
498
581
|
while (stack.length) for (const n of stack.pop()) if (n.k === "slot-outlet") {
|
|
499
582
|
if (n.name) out.add(n.name);
|
|
@@ -503,7 +586,6 @@ function layoutSlots(ws, name) {
|
|
|
503
586
|
else if (n.k === "for") stack.push(n.body, n.empty ?? []);
|
|
504
587
|
else if (n.k === "switch") stack.push(...n.cases.map((c) => c.body), n.default ?? []);
|
|
505
588
|
else if (n.k === "slot-fill") stack.push(n.body);
|
|
506
|
-
layout = ast.layout;
|
|
507
589
|
}
|
|
508
590
|
return [...out];
|
|
509
591
|
}
|
|
@@ -562,14 +644,14 @@ function completions(ws, uri, text, offset) {
|
|
|
562
644
|
const dir = ws.componentsDir();
|
|
563
645
|
return ws.names().filter((n) => !n.startsWith(`${dir}/`)).map((label) => ({
|
|
564
646
|
label,
|
|
565
|
-
kind: FILE
|
|
647
|
+
kind: FILE$1
|
|
566
648
|
}));
|
|
567
649
|
}
|
|
568
650
|
case "slot-name": return layoutSlots(ws, ws.nameOf(uri)).map((label) => ({
|
|
569
651
|
label,
|
|
570
652
|
kind: PROPERTY
|
|
571
653
|
}));
|
|
572
|
-
case "expr": return [
|
|
654
|
+
case "expr": return [.../* @__PURE__ */ new Set([...c.scope, ...FORM_STATE])].map((label) => ({
|
|
573
655
|
label,
|
|
574
656
|
kind: VARIABLE
|
|
575
657
|
}));
|
|
@@ -631,7 +713,7 @@ function posOf(text, offset) {
|
|
|
631
713
|
}
|
|
632
714
|
//#endregion
|
|
633
715
|
//#region src/plugin/definition.ts
|
|
634
|
-
const TOP = {
|
|
716
|
+
const TOP$1 = {
|
|
635
717
|
start: {
|
|
636
718
|
line: 0,
|
|
637
719
|
character: 0
|
|
@@ -648,7 +730,7 @@ function definition(ws, _uri, text, offset) {
|
|
|
648
730
|
start: posOf(text, start),
|
|
649
731
|
end: posOf(text, end)
|
|
650
732
|
};
|
|
651
|
-
const link = (targetUri, targetRange = TOP) => ({
|
|
733
|
+
const link = (targetUri, targetRange = TOP$1) => ({
|
|
652
734
|
originSelectionRange: origin,
|
|
653
735
|
targetUri,
|
|
654
736
|
targetRange,
|
|
@@ -678,6 +760,75 @@ function definition(ws, _uri, text, offset) {
|
|
|
678
760
|
return null;
|
|
679
761
|
}
|
|
680
762
|
//#endregion
|
|
763
|
+
//#region src/plugin/render-calls.ts
|
|
764
|
+
/** A call's template name as typed so far, up to the cursor. */
|
|
765
|
+
const OPEN_CALL = /\b(?:ctx|views)\.render\(\s*(['"`])([^'"`\n]*)$/;
|
|
766
|
+
/** Every call with a literal name. */
|
|
767
|
+
const CALLS = /\b(?:ctx|views)\.render\(\s*(['"`])([^'"`\n]*)\1/g;
|
|
768
|
+
const FILE = 17;
|
|
769
|
+
/** Template names a page can render: everything but components. */
|
|
770
|
+
function renderable(ws) {
|
|
771
|
+
const dir = `${ws.componentsDir()}/`;
|
|
772
|
+
return ws.names().filter((n) => !n.startsWith(dir));
|
|
773
|
+
}
|
|
774
|
+
function renderCompletions(ws, text, offset) {
|
|
775
|
+
const m = OPEN_CALL.exec(text.slice(0, offset));
|
|
776
|
+
if (!m) return void 0;
|
|
777
|
+
const range = {
|
|
778
|
+
start: posOf(text, offset - m[2].length),
|
|
779
|
+
end: posOf(text, offset)
|
|
780
|
+
};
|
|
781
|
+
return renderable(ws).map((name) => ({
|
|
782
|
+
label: name,
|
|
783
|
+
kind: FILE,
|
|
784
|
+
textEdit: {
|
|
785
|
+
range,
|
|
786
|
+
newText: name
|
|
787
|
+
}
|
|
788
|
+
}));
|
|
789
|
+
}
|
|
790
|
+
/** Calls whose name is a plain literal (not a template string with `${…}`). */
|
|
791
|
+
function calls(text) {
|
|
792
|
+
return [...text.matchAll(CALLS)].filter((m) => !m[2].includes("${")).map((m) => ({
|
|
793
|
+
name: m[2],
|
|
794
|
+
offset: m.index + m[0].length - m[2].length - 1
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
function renderDefinition(ws, text, offset) {
|
|
798
|
+
const call = calls(text).find((c) => offset >= c.offset && offset <= c.offset + c.name.length);
|
|
799
|
+
if (!call || !ws.astOf(call.name)) return null;
|
|
800
|
+
const top = {
|
|
801
|
+
start: {
|
|
802
|
+
line: 0,
|
|
803
|
+
character: 0
|
|
804
|
+
},
|
|
805
|
+
end: {
|
|
806
|
+
line: 0,
|
|
807
|
+
character: 0
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
return {
|
|
811
|
+
originSelectionRange: {
|
|
812
|
+
start: posOf(text, call.offset),
|
|
813
|
+
end: posOf(text, call.offset + call.name.length)
|
|
814
|
+
},
|
|
815
|
+
targetUri: ws.uriOf(call.name),
|
|
816
|
+
targetRange: top,
|
|
817
|
+
targetSelectionRange: top
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function renderDiagnostics(ws, text) {
|
|
821
|
+
return calls(text).filter((c) => !ws.astOf(c.name)).map((c) => ({
|
|
822
|
+
range: {
|
|
823
|
+
start: posOf(text, c.offset),
|
|
824
|
+
end: posOf(text, c.offset + c.name.length)
|
|
825
|
+
},
|
|
826
|
+
severity: 2,
|
|
827
|
+
source: "kick",
|
|
828
|
+
message: `template '${c.name}' not found`
|
|
829
|
+
}));
|
|
830
|
+
}
|
|
831
|
+
//#endregion
|
|
681
832
|
//#region src/plugin/diagnostics.ts
|
|
682
833
|
/** The `.kick` file a root virtual-code document belongs to. */
|
|
683
834
|
function sourceUri(context, uri) {
|
|
@@ -692,6 +843,7 @@ function createDiagnosticsPlugin(ws) {
|
|
|
692
843
|
} },
|
|
693
844
|
create(context) {
|
|
694
845
|
return { provideDiagnostics(document) {
|
|
846
|
+
if (document.languageId === "kick-render") return renderDiagnostics(ws, document.getText());
|
|
695
847
|
if (document.languageId !== "kick") return void 0;
|
|
696
848
|
const uri = sourceUri(context, document.uri);
|
|
697
849
|
ws.sync(uri, document.getText());
|
|
@@ -718,6 +870,421 @@ function formatEdits(view, text, file, tabSize) {
|
|
|
718
870
|
}];
|
|
719
871
|
}
|
|
720
872
|
//#endregion
|
|
873
|
+
//#region src/plugin/tokens.ts
|
|
874
|
+
/**
|
|
875
|
+
* Semantic tokens for `.kick` templates, so every LSP editor highlights the
|
|
876
|
+
* template syntax, not only VS Code with its TextMate grammar. Found by a
|
|
877
|
+
* lexical scan, like the language plugin's: the buffer being typed in rarely
|
|
878
|
+
* parses.
|
|
879
|
+
*/
|
|
880
|
+
/** The legend, in order: standard types, so themes colour them. */
|
|
881
|
+
const TOKEN_TYPES = [
|
|
882
|
+
"keyword",
|
|
883
|
+
"comment",
|
|
884
|
+
"operator",
|
|
885
|
+
"function",
|
|
886
|
+
"variable",
|
|
887
|
+
"property",
|
|
888
|
+
"string",
|
|
889
|
+
"number",
|
|
890
|
+
"class",
|
|
891
|
+
"parameter"
|
|
892
|
+
];
|
|
893
|
+
const LITERALS = /* @__PURE__ */ new Set([
|
|
894
|
+
"true",
|
|
895
|
+
"false",
|
|
896
|
+
"null",
|
|
897
|
+
"undefined"
|
|
898
|
+
]);
|
|
899
|
+
/** Words with a meaning in block headers: `@for (x of xs; track x.id; let i = $index)`, `@if (a; as b)`, `@layout(none)`. */
|
|
900
|
+
const HEADER_WORDS = /* @__PURE__ */ new Set([
|
|
901
|
+
"of",
|
|
902
|
+
"track",
|
|
903
|
+
"let",
|
|
904
|
+
"as",
|
|
905
|
+
"none"
|
|
906
|
+
]);
|
|
907
|
+
function kickTokens(s, isComponent) {
|
|
908
|
+
const out = [];
|
|
909
|
+
const push = (offset, length, type) => {
|
|
910
|
+
if (length > 0) out.push({
|
|
911
|
+
offset,
|
|
912
|
+
length,
|
|
913
|
+
type
|
|
914
|
+
});
|
|
915
|
+
};
|
|
916
|
+
/** An expression in `[from, to)`. */
|
|
917
|
+
const expr = (from, to, header) => {
|
|
918
|
+
let prev = "";
|
|
919
|
+
for (let i = from; i < to;) {
|
|
920
|
+
const c = s[i];
|
|
921
|
+
if (/\s/.test(c)) {
|
|
922
|
+
i++;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (c === "'" || c === "\"" || c === "`") {
|
|
926
|
+
let j = i + 1;
|
|
927
|
+
while (j < to && s[j] !== c) j += s[j] === "\\" ? 2 : 1;
|
|
928
|
+
j = Math.min(j + 1, to);
|
|
929
|
+
push(i, j - i, "string");
|
|
930
|
+
prev = "lit";
|
|
931
|
+
i = j;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const num = /^\d+(?:\.\d+)?/.exec(s.slice(i, Math.min(to, i + 32)));
|
|
935
|
+
if (num) {
|
|
936
|
+
push(i, num[0].length, "number");
|
|
937
|
+
prev = "lit";
|
|
938
|
+
i += num[0].length;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const word = /^[A-Za-z_$][\w$]*/.exec(s.slice(i, Math.min(to, i + 128)));
|
|
942
|
+
if (word) {
|
|
943
|
+
const w = word[0];
|
|
944
|
+
const type = prev === "." ? "property" : prev === "|" ? "function" : LITERALS.has(w) || header && HEADER_WORDS.has(w) ? "keyword" : "variable";
|
|
945
|
+
push(i, w.length, type);
|
|
946
|
+
prev = "word";
|
|
947
|
+
i += w.length;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (c === "|" && s[i + 1] === "|") {
|
|
951
|
+
prev = "||";
|
|
952
|
+
i += 2;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (c === "|") push(i, 1, "operator");
|
|
956
|
+
prev = c === "?" && s[i + 1] === "." ? "." : c;
|
|
957
|
+
i += c === "?" && s[i + 1] === "." ? 2 : 1;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
/** `{{ … }}` from `i` (at `{{`); returns its end. */
|
|
961
|
+
const interp = (i, limit) => {
|
|
962
|
+
const end = Math.min(closeInterp(s, i), limit);
|
|
963
|
+
const closed = s.startsWith("}}", end - 2) && end - 2 >= i + 2;
|
|
964
|
+
push(i, 2, "operator");
|
|
965
|
+
expr(i + 2, closed ? end - 2 : end, false);
|
|
966
|
+
if (closed) push(end - 2, 2, "operator");
|
|
967
|
+
return end;
|
|
968
|
+
};
|
|
969
|
+
/** One tag from `i` (at `<`); returns where scanning continues. */
|
|
970
|
+
const tag = (i) => {
|
|
971
|
+
const m = /^<(\/?)([A-Za-z][\w:.-]*)/.exec(s.slice(i, i + 64));
|
|
972
|
+
const name = m[2].toLowerCase();
|
|
973
|
+
if (isComponent(name)) push(i + 1 + m[1].length, m[2].length, "class");
|
|
974
|
+
let j = i + m[0].length;
|
|
975
|
+
while (j < s.length && s[j] !== ">") {
|
|
976
|
+
const binding = /^\[([^\]\s]+)\]\s*=\s*(["'])/.exec(s.slice(j, j + 128));
|
|
977
|
+
if (binding) {
|
|
978
|
+
push(j + 1, binding[1].length, "property");
|
|
979
|
+
const from = j + binding[0].length;
|
|
980
|
+
const close = s.indexOf(binding[2], from);
|
|
981
|
+
const stop = close < 0 ? s.length : close;
|
|
982
|
+
expr(from, stop, false);
|
|
983
|
+
j = stop + 1;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const q = s[j];
|
|
987
|
+
if (q === "\"" || q === "'") {
|
|
988
|
+
const close = s.indexOf(q, j + 1);
|
|
989
|
+
const stop = close < 0 ? s.length : close;
|
|
990
|
+
for (let k = j + 1; k < stop; k++) if (s.startsWith("{{", k)) k = interp(k, stop) - 1;
|
|
991
|
+
j = stop + 1;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
j++;
|
|
995
|
+
}
|
|
996
|
+
j = Math.min(j + 1, s.length);
|
|
997
|
+
if (m[1] || name !== "script" && name !== "style" || s[j - 2] === "/") return j;
|
|
998
|
+
const close = new RegExp(`</${name}(?=[\\t\\n\\f\\r />])`, "i").exec(s.slice(j));
|
|
999
|
+
const end = close ? j + close.index : s.length;
|
|
1000
|
+
for (let k = j; k < end; k++) if (s.startsWith("{{", k)) k = interp(k, end) - 1;
|
|
1001
|
+
return end;
|
|
1002
|
+
};
|
|
1003
|
+
let i = 0;
|
|
1004
|
+
while (i < s.length) {
|
|
1005
|
+
const c = s[i];
|
|
1006
|
+
if (s.startsWith("{{--", i)) {
|
|
1007
|
+
const end = s.indexOf("--}}", i + 4);
|
|
1008
|
+
const stop = end < 0 ? s.length : end + 4;
|
|
1009
|
+
push(i, stop - i, "comment");
|
|
1010
|
+
i = stop;
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (s.startsWith("{{", i)) {
|
|
1014
|
+
i = interp(i, s.length);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (s.startsWith("<!--", i)) {
|
|
1018
|
+
const end = s.indexOf("-->", i + 4);
|
|
1019
|
+
i = end < 0 ? s.length : end + 3;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (c === "<" && /^<\/?[A-Za-z]/.test(s.slice(i, i + 3))) {
|
|
1023
|
+
i = tag(i);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (c === "@" && !/[\w@.-]/.test(s[i - 1] ?? "")) {
|
|
1027
|
+
const m = KEYWORD$1.exec(s.slice(i, i + 16));
|
|
1028
|
+
if (m) {
|
|
1029
|
+
const kw = m[1].replace(/\s+/g, " ");
|
|
1030
|
+
push(i, m[0].length, "keyword");
|
|
1031
|
+
let j = i + m[0].length;
|
|
1032
|
+
if (kw === "input" || kw === "let") {
|
|
1033
|
+
const decl = /^\s+([\w$]+)/.exec(s.slice(j, j + 64));
|
|
1034
|
+
if (decl) push(j + decl[0].length - decl[1].length, decl[1].length, kw === "input" ? "parameter" : "variable");
|
|
1035
|
+
const eol = s.indexOf("\n", j);
|
|
1036
|
+
const stop = kw === "let" ? letEnd(s, j).at : eol < 0 ? s.length : eol;
|
|
1037
|
+
const eq = s.indexOf("=", j);
|
|
1038
|
+
if (eq >= 0 && eq < stop) expr(eq + 1, stop, false);
|
|
1039
|
+
i = stop;
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
1043
|
+
if (kw === "verbatim" && s[j] === "{") {
|
|
1044
|
+
i = verbatimEnd(s, j);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (s[j] === "(") {
|
|
1048
|
+
const end = closeParen$1(s, j);
|
|
1049
|
+
expr(j + 1, s[end - 1] === ")" ? end - 1 : end, true);
|
|
1050
|
+
i = end;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
i = i + m[0].length;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
i++;
|
|
1058
|
+
}
|
|
1059
|
+
return out;
|
|
1060
|
+
}
|
|
1061
|
+
//#endregion
|
|
1062
|
+
//#region src/plugin/references.ts
|
|
1063
|
+
const TOP = {
|
|
1064
|
+
start: {
|
|
1065
|
+
line: 0,
|
|
1066
|
+
character: 0
|
|
1067
|
+
},
|
|
1068
|
+
end: {
|
|
1069
|
+
line: 0,
|
|
1070
|
+
character: 0
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
1074
|
+
const LAYOUT = /@layout\s*\(\s*(['"])([^'"]*)\1/g;
|
|
1075
|
+
const rangeOf = (text, offset, length) => ({
|
|
1076
|
+
start: posOf(text, offset),
|
|
1077
|
+
end: posOf(text, offset + length)
|
|
1078
|
+
});
|
|
1079
|
+
/** The shared name under the cursor, with where it sits in `text`. */
|
|
1080
|
+
function targetAt(ws, text, offset) {
|
|
1081
|
+
const t = kickTokens(text, (tag) => componentOf(ws, tag) !== void 0).find((k) => offset >= k.offset && offset <= k.offset + k.length);
|
|
1082
|
+
if (!t) return void 0;
|
|
1083
|
+
const word = text.slice(t.offset, t.offset + t.length);
|
|
1084
|
+
if (t.type === "class") {
|
|
1085
|
+
const name = componentOf(ws, word.toLowerCase());
|
|
1086
|
+
return name === void 0 ? void 0 : {
|
|
1087
|
+
target: {
|
|
1088
|
+
kind: "component",
|
|
1089
|
+
name,
|
|
1090
|
+
tag: word.toLowerCase()
|
|
1091
|
+
},
|
|
1092
|
+
...t
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
if (t.type === "function") return {
|
|
1096
|
+
target: {
|
|
1097
|
+
kind: "pipe",
|
|
1098
|
+
name: word
|
|
1099
|
+
},
|
|
1100
|
+
...t
|
|
1101
|
+
};
|
|
1102
|
+
if (t.type === "string" && /@layout\s*\(\s*$/.test(text.slice(0, t.offset))) {
|
|
1103
|
+
const name = word.slice(1, -1);
|
|
1104
|
+
return ws.astOf(name) ? {
|
|
1105
|
+
target: {
|
|
1106
|
+
kind: "layout",
|
|
1107
|
+
name
|
|
1108
|
+
},
|
|
1109
|
+
offset: t.offset + 1,
|
|
1110
|
+
length: name.length
|
|
1111
|
+
} : void 0;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/** Every use of `target` in the templates. */
|
|
1115
|
+
function usesOf(ws, target) {
|
|
1116
|
+
const out = [];
|
|
1117
|
+
for (const name of ws.names()) {
|
|
1118
|
+
const text = ws.textOf(name);
|
|
1119
|
+
if (text === void 0) continue;
|
|
1120
|
+
const uri = ws.uriOf(name);
|
|
1121
|
+
if (target.kind === "layout") {
|
|
1122
|
+
for (const m of text.matchAll(LAYOUT)) if (m[2] === target.name) out.push({
|
|
1123
|
+
uri,
|
|
1124
|
+
text,
|
|
1125
|
+
offset: m.index + m[0].length - m[2].length - 1,
|
|
1126
|
+
length: m[2].length
|
|
1127
|
+
});
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const want = target.kind === "component" ? "class" : "function";
|
|
1131
|
+
const match = target.kind === "component" ? target.tag : target.name;
|
|
1132
|
+
for (const t of kickTokens(text, (tag) => target.kind === "component" && tag === target.tag)) if (t.type === want && text.slice(t.offset, t.offset + t.length).toLowerCase() === match.toLowerCase()) out.push({
|
|
1133
|
+
uri,
|
|
1134
|
+
text,
|
|
1135
|
+
...t
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return out;
|
|
1139
|
+
}
|
|
1140
|
+
/** Where a pipe is declared in `pipes.ts`; only the forms the server can read. */
|
|
1141
|
+
function pipeDeclaration(ws, name) {
|
|
1142
|
+
const decl = ws.pipeDeclaration(name);
|
|
1143
|
+
if (!decl) return void 0;
|
|
1144
|
+
const start = {
|
|
1145
|
+
line: decl.line,
|
|
1146
|
+
character: decl.column
|
|
1147
|
+
};
|
|
1148
|
+
return {
|
|
1149
|
+
uri: URI.file(ws.pipesFile()).toString(),
|
|
1150
|
+
range: {
|
|
1151
|
+
start,
|
|
1152
|
+
end: {
|
|
1153
|
+
line: decl.line,
|
|
1154
|
+
character: decl.column + name.length
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function references(ws, text, offset, includeDeclaration) {
|
|
1160
|
+
const at = targetAt(ws, text, offset);
|
|
1161
|
+
if (!at) return [];
|
|
1162
|
+
const { target } = at;
|
|
1163
|
+
const locations = usesOf(ws, target).map((u) => ({
|
|
1164
|
+
uri: u.uri,
|
|
1165
|
+
range: rangeOf(u.text, u.offset, u.length)
|
|
1166
|
+
}));
|
|
1167
|
+
if (includeDeclaration) {
|
|
1168
|
+
if (target.kind === "pipe") {
|
|
1169
|
+
const decl = pipeDeclaration(ws, target.name);
|
|
1170
|
+
if (decl) locations.push(decl);
|
|
1171
|
+
} else locations.push({
|
|
1172
|
+
uri: ws.uriOf(target.name),
|
|
1173
|
+
range: TOP
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
return locations;
|
|
1177
|
+
}
|
|
1178
|
+
/** What F2 renames here: a pipe, a component tag or a layout name. */
|
|
1179
|
+
function renameRange(ws, text, offset) {
|
|
1180
|
+
const at = targetAt(ws, text, offset);
|
|
1181
|
+
if (!at) return void 0;
|
|
1182
|
+
if (at.target.kind === "pipe" && !pipeDeclaration(ws, at.target.name)) return { message: `Rename ${at.target.name} in your pipes file; it isn't declared in a form the editor can rename.` };
|
|
1183
|
+
return rangeOf(text, at.offset, at.length);
|
|
1184
|
+
}
|
|
1185
|
+
const TAG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
|
|
1186
|
+
/** The component file for `tag`, kept in the old one's folder when the tag still starts with it. */
|
|
1187
|
+
function componentNameFor(ws, oldName, tag) {
|
|
1188
|
+
if (!TAG.test(tag)) return void 0;
|
|
1189
|
+
const dir = ws.componentsDir();
|
|
1190
|
+
const folders = oldName.slice(dir.length + 1).split("/").slice(0, -1);
|
|
1191
|
+
const prefix = folders.join("-");
|
|
1192
|
+
const name = prefix && tag.startsWith(`${prefix}-`) ? `${dir}/${folders.join("/")}/${tag.slice(prefix.length + 1)}` : `${dir}/${tag}`;
|
|
1193
|
+
return ws.view.componentTag(name, dir) === tag ? name : void 0;
|
|
1194
|
+
}
|
|
1195
|
+
/** Every use of `target` replaced with `newText`, then the template's file renamed. */
|
|
1196
|
+
function renameTemplate(ws, target, newText, newName) {
|
|
1197
|
+
if (target.kind === "pipe") return void 0;
|
|
1198
|
+
if (ws.view.templateName(newName) !== newName || ws.astOf(newName)) return void 0;
|
|
1199
|
+
const byUri = /* @__PURE__ */ new Map();
|
|
1200
|
+
for (const u of usesOf(ws, target)) {
|
|
1201
|
+
const edits = byUri.get(u.uri) ?? [];
|
|
1202
|
+
edits.push({
|
|
1203
|
+
range: rangeOf(u.text, u.offset, u.length),
|
|
1204
|
+
newText
|
|
1205
|
+
});
|
|
1206
|
+
byUri.set(u.uri, edits);
|
|
1207
|
+
}
|
|
1208
|
+
return { documentChanges: [...[...byUri].map(([uri, edits]) => ({
|
|
1209
|
+
textDocument: {
|
|
1210
|
+
uri,
|
|
1211
|
+
version: null
|
|
1212
|
+
},
|
|
1213
|
+
edits
|
|
1214
|
+
})), {
|
|
1215
|
+
kind: "rename",
|
|
1216
|
+
oldUri: ws.uriOf(target.name),
|
|
1217
|
+
newUri: ws.uriOf(newName)
|
|
1218
|
+
}] };
|
|
1219
|
+
}
|
|
1220
|
+
/** A pipe renamed in every template and in `pipes.ts`; a component or layout, file included. */
|
|
1221
|
+
function renameEdits(ws, text, offset, newName) {
|
|
1222
|
+
const at = targetAt(ws, text, offset);
|
|
1223
|
+
if (!at) return void 0;
|
|
1224
|
+
const { target } = at;
|
|
1225
|
+
if (target.kind === "component") {
|
|
1226
|
+
const name = componentNameFor(ws, target.name, newName);
|
|
1227
|
+
return name === void 0 ? void 0 : renameTemplate(ws, target, newName, name);
|
|
1228
|
+
}
|
|
1229
|
+
if (target.kind === "layout") return renameTemplate(ws, target, newName, newName);
|
|
1230
|
+
if (!IDENT.test(newName) || ws.project().pipes.has(newName)) return void 0;
|
|
1231
|
+
const decl = pipeDeclaration(ws, target.name);
|
|
1232
|
+
if (!decl) return void 0;
|
|
1233
|
+
const changes = {};
|
|
1234
|
+
const add = (uri, range) => (changes[uri] ??= []).push({
|
|
1235
|
+
range,
|
|
1236
|
+
newText: newName
|
|
1237
|
+
});
|
|
1238
|
+
for (const u of usesOf(ws, target)) add(u.uri, rangeOf(u.text, u.offset, u.length));
|
|
1239
|
+
add(decl.uri, decl.range);
|
|
1240
|
+
return { changes };
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Edits that keep uses pointing at renamed templates: a component's tags, a
|
|
1244
|
+
* layout's `@layout('…')`, a page's `ctx.render('…')`. `oldUri` / `newUri`
|
|
1245
|
+
* can be a file or a folder.
|
|
1246
|
+
*/
|
|
1247
|
+
async function fileRenameEdits(ws, oldUri, newUri) {
|
|
1248
|
+
const from = ws.relOf(oldUri)?.replace(/\.kick$/, "");
|
|
1249
|
+
const to = ws.relOf(newUri)?.replace(/\.kick$/, "");
|
|
1250
|
+
if (from === void 0 || to === void 0 || from === to) return void 0;
|
|
1251
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
1252
|
+
for (const name of ws.names()) if (name === from) renamed.set(name, to);
|
|
1253
|
+
else if (name.startsWith(`${from}/`)) renamed.set(name, to + name.slice(from.length));
|
|
1254
|
+
if (!renamed.size) return void 0;
|
|
1255
|
+
const changes = {};
|
|
1256
|
+
const dir = ws.componentsDir();
|
|
1257
|
+
for (const [oldName, newName] of renamed) {
|
|
1258
|
+
const comp = oldName.startsWith(`${dir}/`);
|
|
1259
|
+
const newTag = newName.startsWith(`${dir}/`) ? ws.view.componentTag(newName, dir) : void 0;
|
|
1260
|
+
const targets = [[{
|
|
1261
|
+
kind: "layout",
|
|
1262
|
+
name: oldName
|
|
1263
|
+
}, newName]];
|
|
1264
|
+
if (comp && newTag) targets.push([{
|
|
1265
|
+
kind: "component",
|
|
1266
|
+
name: oldName,
|
|
1267
|
+
tag: ws.view.componentTag(oldName, dir)
|
|
1268
|
+
}, newTag]);
|
|
1269
|
+
for (const [target, newText] of targets) for (const u of usesOf(ws, target)) (changes[u.uri] ??= []).push({
|
|
1270
|
+
range: rangeOf(u.text, u.offset, u.length),
|
|
1271
|
+
newText
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
const pages = [...renamed].filter(([n]) => !n.startsWith(`${dir}/`));
|
|
1275
|
+
if (pages.length) {
|
|
1276
|
+
const to = new Map(pages);
|
|
1277
|
+
for (const script of await ws.scripts()) for (const c of calls(script.text)) {
|
|
1278
|
+
const newName = to.get(c.name);
|
|
1279
|
+
if (newName !== void 0) (changes[script.uri] ??= []).push({
|
|
1280
|
+
range: rangeOf(script.text, c.offset, c.name.length),
|
|
1281
|
+
newText: newName
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return Object.keys(changes).length ? { changes } : void 0;
|
|
1286
|
+
}
|
|
1287
|
+
//#endregion
|
|
721
1288
|
//#region src/plugin/index.ts
|
|
722
1289
|
function createKickServicePlugin(ws) {
|
|
723
1290
|
const diagnostics = createDiagnosticsPlugin(ws);
|
|
@@ -735,7 +1302,14 @@ function createKickServicePlugin(ws) {
|
|
|
735
1302
|
] },
|
|
736
1303
|
hoverProvider: true,
|
|
737
1304
|
definitionProvider: true,
|
|
738
|
-
documentFormattingProvider: true
|
|
1305
|
+
documentFormattingProvider: true,
|
|
1306
|
+
semanticTokensProvider: { legend: {
|
|
1307
|
+
tokenTypes: [...TOKEN_TYPES],
|
|
1308
|
+
tokenModifiers: []
|
|
1309
|
+
} },
|
|
1310
|
+
referencesProvider: true,
|
|
1311
|
+
renameProvider: { prepareProvider: true },
|
|
1312
|
+
fileRenameEditsProvider: true
|
|
739
1313
|
},
|
|
740
1314
|
create(context) {
|
|
741
1315
|
/** The source file's uri and text, with the workspace synced to the buffer. */
|
|
@@ -752,6 +1326,13 @@ function createKickServicePlugin(ws) {
|
|
|
752
1326
|
return {
|
|
753
1327
|
...diagnostics.create(context),
|
|
754
1328
|
provideCompletionItems(document, position) {
|
|
1329
|
+
if (document.languageId === "kick-render") {
|
|
1330
|
+
const items = renderCompletions(ws, document.getText(), document.offsetAt(position));
|
|
1331
|
+
return items && {
|
|
1332
|
+
isIncomplete: false,
|
|
1333
|
+
items
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
755
1336
|
const d = open(document);
|
|
756
1337
|
if (!d) return void 0;
|
|
757
1338
|
const items = completions(ws, d.uri, d.text, document.offsetAt(position));
|
|
@@ -765,10 +1346,51 @@ function createKickServicePlugin(ws) {
|
|
|
765
1346
|
return d ? hover(ws, d.uri, d.text, document.offsetAt(position)) ?? void 0 : void 0;
|
|
766
1347
|
},
|
|
767
1348
|
provideDefinition(document, position) {
|
|
1349
|
+
if (document.languageId === "kick-render") {
|
|
1350
|
+
const link = renderDefinition(ws, document.getText(), document.offsetAt(position));
|
|
1351
|
+
return link ? [link] : void 0;
|
|
1352
|
+
}
|
|
768
1353
|
const d = open(document);
|
|
769
1354
|
const link = d && definition(ws, d.uri, d.text, document.offsetAt(position));
|
|
770
1355
|
return link ? [link] : void 0;
|
|
771
1356
|
},
|
|
1357
|
+
provideReferences(document, position, context) {
|
|
1358
|
+
const d = open(document);
|
|
1359
|
+
return d ? references(ws, d.text, document.offsetAt(position), context.includeDeclaration) : void 0;
|
|
1360
|
+
},
|
|
1361
|
+
provideRenameRange(document, position) {
|
|
1362
|
+
const d = open(document);
|
|
1363
|
+
return d ? renameRange(ws, d.text, document.offsetAt(position)) : void 0;
|
|
1364
|
+
},
|
|
1365
|
+
provideRenameEdits(document, position, newName) {
|
|
1366
|
+
const d = open(document);
|
|
1367
|
+
return d ? renameEdits(ws, d.text, document.offsetAt(position), newName) : void 0;
|
|
1368
|
+
},
|
|
1369
|
+
provideFileRenameEdits(oldUri, newUri) {
|
|
1370
|
+
return fileRenameEdits(ws, oldUri.toString(), newUri.toString());
|
|
1371
|
+
},
|
|
1372
|
+
provideDocumentSemanticTokens(document, range, legend) {
|
|
1373
|
+
const d = open(document);
|
|
1374
|
+
if (!d) return void 0;
|
|
1375
|
+
const tokens = [];
|
|
1376
|
+
for (const t of kickTokens(d.text, (tag) => componentOf(ws, tag) !== void 0)) {
|
|
1377
|
+
const type = legend.tokenTypes.indexOf(t.type);
|
|
1378
|
+
if (type < 0) continue;
|
|
1379
|
+
let at = t.offset;
|
|
1380
|
+
for (const part of d.text.slice(t.offset, t.offset + t.length).split("\n")) {
|
|
1381
|
+
const p = document.positionAt(at);
|
|
1382
|
+
if (part && p.line >= range.start.line && p.line <= range.end.line) tokens.push([
|
|
1383
|
+
p.line,
|
|
1384
|
+
p.character,
|
|
1385
|
+
part.length,
|
|
1386
|
+
type,
|
|
1387
|
+
0
|
|
1388
|
+
]);
|
|
1389
|
+
at += part.length + 1;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return tokens;
|
|
1393
|
+
},
|
|
772
1394
|
provideDocumentFormattingEdits(document, _range, options) {
|
|
773
1395
|
const d = open(document);
|
|
774
1396
|
if (!d) return void 0;
|
|
@@ -952,6 +1574,11 @@ var Workspace = class {
|
|
|
952
1574
|
if (rel.startsWith("..") || !rel.endsWith(".kick")) return void 0;
|
|
953
1575
|
return this.opts.view.templateName(rel.slice(0, -5));
|
|
954
1576
|
}
|
|
1577
|
+
/** `uri`'s path inside the views folder (`layouts/main.kick`, `layouts`); `undefined` outside it. */
|
|
1578
|
+
relOf(uri) {
|
|
1579
|
+
const rel = relative(this.opts.viewsDir, URI.parse(uri).fsPath).split(sep).join(posix.sep);
|
|
1580
|
+
return rel.startsWith("..") || isAbsolute(rel) ? void 0 : rel;
|
|
1581
|
+
}
|
|
955
1582
|
nameOf(uri) {
|
|
956
1583
|
return this.nameOfPath(URI.parse(uri).fsPath);
|
|
957
1584
|
}
|
|
@@ -1069,6 +1696,7 @@ var Workspace = class {
|
|
|
1069
1696
|
}
|
|
1070
1697
|
/** Templates that use `name` as a component or a layout — a layout even after it's gone. */
|
|
1071
1698
|
dependentsOf(name) {
|
|
1699
|
+
if (this.configuredLayouts().has(name)) return new Set(this.names().filter((n) => n !== name));
|
|
1072
1700
|
const out = /* @__PURE__ */ new Set();
|
|
1073
1701
|
const tag = name.startsWith(`${this.opts.componentsDir}/`) ? this.opts.view.componentTag(name, this.opts.componentsDir) : void 0;
|
|
1074
1702
|
for (const [n, e] of this.entries) {
|
|
@@ -1091,9 +1719,38 @@ var Workspace = class {
|
|
|
1091
1719
|
return {
|
|
1092
1720
|
templates: new Map([...this.entries].map(([n, e]) => [n, e.ast])),
|
|
1093
1721
|
pipes: new Set(this.pipeNames()),
|
|
1094
|
-
componentsDir: this.opts.componentsDir
|
|
1722
|
+
componentsDir: this.opts.componentsDir,
|
|
1723
|
+
layouts: this.opts.layout,
|
|
1724
|
+
baseLayout: this.opts.baseLayout
|
|
1095
1725
|
};
|
|
1096
1726
|
}
|
|
1727
|
+
/** Templates that are layouts by configuration: the mapping's targets and the base layout. */
|
|
1728
|
+
configuredLayouts() {
|
|
1729
|
+
const { layout, baseLayout } = this.opts;
|
|
1730
|
+
const out = new Set(typeof layout === "string" ? [layout] : Object.values(layout ?? {}));
|
|
1731
|
+
if (baseLayout) out.add(baseLayout);
|
|
1732
|
+
return out;
|
|
1733
|
+
}
|
|
1734
|
+
scripts() {
|
|
1735
|
+
return this.opts.scripts?.() ?? Promise.resolve([]);
|
|
1736
|
+
}
|
|
1737
|
+
baseLayout() {
|
|
1738
|
+
return this.opts.baseLayout;
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* The layout `name` renders in: its own `@layout`, else the configured
|
|
1742
|
+
* mapping's longest matching folder prefix (pages only, as `check` does).
|
|
1743
|
+
*/
|
|
1744
|
+
layoutOf(name) {
|
|
1745
|
+
const ast = this.astOf(name);
|
|
1746
|
+
if (!ast) return void 0;
|
|
1747
|
+
if (ast.layout !== void 0) return ast.layout ?? void 0;
|
|
1748
|
+
const { layout } = this.opts;
|
|
1749
|
+
if (layout === void 0 || name.startsWith(`${this.opts.componentsDir}/`) || this.configuredLayouts().has(name)) return void 0;
|
|
1750
|
+
if (typeof layout === "string") return layout;
|
|
1751
|
+
const hit = Object.keys(layout).toSorted((a, b) => b.length - a.length).find((p) => p === "" || name === p || name.startsWith(`${p}/`));
|
|
1752
|
+
return hit === void 0 ? void 0 : layout[hit];
|
|
1753
|
+
}
|
|
1097
1754
|
/** Parse errors, else everything `check` reports — as LSP diagnostics. */
|
|
1098
1755
|
diagnosticsFor(uri) {
|
|
1099
1756
|
const name = this.nameOf(uri);
|
|
@@ -1126,6 +1783,47 @@ async function listKick(dir) {
|
|
|
1126
1783
|
return [];
|
|
1127
1784
|
}
|
|
1128
1785
|
}
|
|
1786
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
1787
|
+
"node_modules",
|
|
1788
|
+
"dist",
|
|
1789
|
+
"build",
|
|
1790
|
+
"coverage",
|
|
1791
|
+
"out"
|
|
1792
|
+
]);
|
|
1793
|
+
const SCRIPT = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
1794
|
+
/** TS / JS files under `root`, for `ctx.render('…')` names: no dependencies, build output, dot folders or `.d.ts`. */
|
|
1795
|
+
async function listScripts(root) {
|
|
1796
|
+
const out = [];
|
|
1797
|
+
const walk = async (dir) => {
|
|
1798
|
+
let entries;
|
|
1799
|
+
try {
|
|
1800
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1801
|
+
} catch {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
for (const e of entries) {
|
|
1805
|
+
if (e.name.startsWith(".") || SKIP.has(e.name)) continue;
|
|
1806
|
+
const p = join(dir, e.name);
|
|
1807
|
+
if (e.isDirectory()) await walk(p);
|
|
1808
|
+
else if (SCRIPT.test(e.name) && !e.name.endsWith(".d.ts")) out.push(p);
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
await walk(root);
|
|
1812
|
+
return out;
|
|
1813
|
+
}
|
|
1814
|
+
/** `fn` over `items` with at most `limit` running at once, results in order. */
|
|
1815
|
+
async function mapLimit(items, limit, fn) {
|
|
1816
|
+
const out = Array.from({ length: items.length });
|
|
1817
|
+
let next = 0;
|
|
1818
|
+
const worker = async () => {
|
|
1819
|
+
while (next < items.length) {
|
|
1820
|
+
const i = next++;
|
|
1821
|
+
out[i] = await fn(items[i]);
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
1825
|
+
return out;
|
|
1826
|
+
}
|
|
1129
1827
|
/** What the server watches: every template, and the pipes file wherever it's configured. */
|
|
1130
1828
|
function watchPatterns(root, pipesFile) {
|
|
1131
1829
|
const patterns = ["**/*.kick", "**/pipes.{ts,js}"];
|
|
@@ -1155,14 +1853,23 @@ function startServer() {
|
|
|
1155
1853
|
pipesFile,
|
|
1156
1854
|
view,
|
|
1157
1855
|
read: (p) => readFile(p, "utf8").catch(() => void 0),
|
|
1158
|
-
list: listKick
|
|
1856
|
+
list: listKick,
|
|
1857
|
+
layout: opts.layout || void 0,
|
|
1858
|
+
baseLayout: opts.baseLayout || void 0,
|
|
1859
|
+
scripts: async () => mapLimit(await listScripts(root), 64, async (p) => {
|
|
1860
|
+
const uri = URI.file(p).toString();
|
|
1861
|
+
return {
|
|
1862
|
+
uri,
|
|
1863
|
+
text: server.documents.get(URI.parse(uri))?.getText() ?? await readFile(p, "utf8").catch(() => "")
|
|
1864
|
+
};
|
|
1865
|
+
})
|
|
1159
1866
|
});
|
|
1160
1867
|
await ws.load();
|
|
1161
1868
|
return server.initialize(params, createSimpleProject([kickLanguagePlugin]), [
|
|
1869
|
+
createKickServicePlugin(ws),
|
|
1162
1870
|
create$2(),
|
|
1163
1871
|
create(),
|
|
1164
|
-
create$1()
|
|
1165
|
-
createKickServicePlugin(ws)
|
|
1872
|
+
create$1()
|
|
1166
1873
|
]);
|
|
1167
1874
|
});
|
|
1168
1875
|
connection.onInitialized(() => {
|
|
@@ -1191,9 +1898,17 @@ function startServer() {
|
|
|
1191
1898
|
const dir = `${ws.componentsDir()}/`;
|
|
1192
1899
|
return ws.names().filter((n) => !n.startsWith(dir));
|
|
1193
1900
|
});
|
|
1901
|
+
connection.onRequest("kick/renderDefinition", ({ text, offset }) => {
|
|
1902
|
+
const link = ws && renderDefinition(ws, text, offset);
|
|
1903
|
+
return link ? {
|
|
1904
|
+
uri: link.targetUri,
|
|
1905
|
+
range: link.originSelectionRange
|
|
1906
|
+
} : null;
|
|
1907
|
+
});
|
|
1908
|
+
connection.onRequest("kick/renderDiagnostics", ({ text }) => ws ? renderDiagnostics(ws, text) : []);
|
|
1194
1909
|
connection.onShutdown(() => server.shutdown());
|
|
1195
1910
|
}
|
|
1196
1911
|
//#endregion
|
|
1197
1912
|
export { KickVirtualCode as a, createKickServicePlugin as i, Workspace as n, kickLanguagePlugin as o, loadViewModule as r, startServer as t };
|
|
1198
1913
|
|
|
1199
|
-
//# sourceMappingURL=server-
|
|
1914
|
+
//# sourceMappingURL=server-Biz_hJF3.js.map
|