@forinda/kickjs-view-language-server 0.1.1 → 0.2.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-view-language-server v0.1.1
2
+ * @forinda/kickjs-view-language-server v0.2.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -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 stop = semi >= 0 && (eol < 0 || semi < eol) ? semi + 1 : eol < 0 ? s.length : eol;
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
- 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);
163
+ const stop = verbatimEnd(s, b);
126
164
  out.blank.push([i, stop]);
127
165
  i = stop;
128
166
  continue;
@@ -277,12 +315,38 @@ var KickVirtualCode = class {
277
315
  ];
278
316
  }
279
317
  };
318
+ /** TS / JS: a code only the kick service plugin reads, for `ctx.render('…')` names. */
319
+ const SCRIPT_IDS = /* @__PURE__ */ new Set([
320
+ "typescript",
321
+ "typescriptreact",
322
+ "javascript",
323
+ "javascriptreact"
324
+ ]);
325
+ const SCRIPT_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
326
+ var RenderCallsCode = class {
327
+ id = "root";
328
+ languageId = "kick-render";
329
+ snapshot;
330
+ mappings;
331
+ embeddedCodes = [];
332
+ constructor(text) {
333
+ this.snapshot = snapshotOf(text);
334
+ this.mappings = [{
335
+ sourceOffsets: [0],
336
+ generatedOffsets: [0],
337
+ lengths: [text.length],
338
+ data: EMBEDDED
339
+ }];
340
+ }
341
+ };
280
342
  const kickLanguagePlugin = {
281
343
  getLanguageId(uri) {
282
344
  return uri.path.endsWith(".kick") ? "kick" : void 0;
283
345
  },
284
- createVirtualCode(_uri, languageId, snapshot) {
285
- return languageId === "kick" ? new KickVirtualCode(snapshot.getText(0, snapshot.getLength())) : void 0;
346
+ createVirtualCode(uri, languageId, snapshot) {
347
+ const text = snapshot.getText(0, snapshot.getLength());
348
+ if (languageId === "kick" || uri.path.endsWith(".kick")) return new KickVirtualCode(text);
349
+ if (SCRIPT_IDS.has(languageId) || SCRIPT_FILE.test(uri.path)) return new RenderCallsCode(text);
286
350
  }
287
351
  };
288
352
  //#endregion
@@ -295,7 +359,7 @@ const LOOP_VARS = [
295
359
  "$odd",
296
360
  "$count"
297
361
  ];
298
- const IDENT = /[A-Za-z_$][\w$]*/;
362
+ const IDENT$1 = /[A-Za-z_$][\w$]*/;
299
363
  /** The end of `(`…`)` from `i` (at `(`), or -1 when it doesn't close before `stop`. */
300
364
  function closeParen(s, i, stop) {
301
365
  let depth = 0;
@@ -354,7 +418,7 @@ function scopeAt(s, stop) {
354
418
  const kw = m[1].replace(/\s+/g, " ");
355
419
  let j = i + m[0].length;
356
420
  if (kw === "input") {
357
- const name = IDENT.exec(s.slice(j, j + 64));
421
+ const name = IDENT$1.exec(s.slice(j, j + 64));
358
422
  if (name) inputs.push(name[0]);
359
423
  i = j;
360
424
  continue;
@@ -462,7 +526,7 @@ const KEYWORD = 14;
462
526
  const FUNCTION = 3;
463
527
  const CLASS = 7;
464
528
  const PROPERTY = 10;
465
- const FILE = 17;
529
+ const FILE$1 = 17;
466
530
  const VARIABLE = 6;
467
531
  const KEYWORDS = {
468
532
  if: "if (${1:condition}) {\n $0\n}",
@@ -489,11 +553,19 @@ function componentOf(ws, tag) {
489
553
  function layoutSlots(ws, name) {
490
554
  const out = /* @__PURE__ */ new Set();
491
555
  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)) {
556
+ const chain = [];
557
+ let layout = name === void 0 ? void 0 : ws.layoutOf(name);
558
+ while (layout !== void 0 && !seen.has(layout)) {
494
559
  seen.add(layout);
495
- const ast = ws.astOf(layout);
496
- if (!ast) break;
560
+ chain.push(layout);
561
+ layout = ws.layoutOf(layout);
562
+ }
563
+ const base = ws.baseLayout();
564
+ const page = name === void 0 ? void 0 : ws.astOf(name);
565
+ if (base !== void 0 && name !== void 0 && page?.layout !== null && !name.startsWith(`${ws.componentsDir()}/`) && name !== base && !seen.has(base)) chain.push(base);
566
+ for (const l of chain) {
567
+ const ast = ws.astOf(l);
568
+ if (!ast) continue;
497
569
  const stack = [ast.body];
498
570
  while (stack.length) for (const n of stack.pop()) if (n.k === "slot-outlet") {
499
571
  if (n.name) out.add(n.name);
@@ -503,7 +575,6 @@ function layoutSlots(ws, name) {
503
575
  else if (n.k === "for") stack.push(n.body, n.empty ?? []);
504
576
  else if (n.k === "switch") stack.push(...n.cases.map((c) => c.body), n.default ?? []);
505
577
  else if (n.k === "slot-fill") stack.push(n.body);
506
- layout = ast.layout;
507
578
  }
508
579
  return [...out];
509
580
  }
@@ -562,7 +633,7 @@ function completions(ws, uri, text, offset) {
562
633
  const dir = ws.componentsDir();
563
634
  return ws.names().filter((n) => !n.startsWith(`${dir}/`)).map((label) => ({
564
635
  label,
565
- kind: FILE
636
+ kind: FILE$1
566
637
  }));
567
638
  }
568
639
  case "slot-name": return layoutSlots(ws, ws.nameOf(uri)).map((label) => ({
@@ -631,7 +702,7 @@ function posOf(text, offset) {
631
702
  }
632
703
  //#endregion
633
704
  //#region src/plugin/definition.ts
634
- const TOP = {
705
+ const TOP$1 = {
635
706
  start: {
636
707
  line: 0,
637
708
  character: 0
@@ -648,7 +719,7 @@ function definition(ws, _uri, text, offset) {
648
719
  start: posOf(text, start),
649
720
  end: posOf(text, end)
650
721
  };
651
- const link = (targetUri, targetRange = TOP) => ({
722
+ const link = (targetUri, targetRange = TOP$1) => ({
652
723
  originSelectionRange: origin,
653
724
  targetUri,
654
725
  targetRange,
@@ -678,6 +749,75 @@ function definition(ws, _uri, text, offset) {
678
749
  return null;
679
750
  }
680
751
  //#endregion
752
+ //#region src/plugin/render-calls.ts
753
+ /** A call's template name as typed so far, up to the cursor. */
754
+ const OPEN_CALL = /\b(?:ctx|views)\.render\(\s*(['"`])([^'"`\n]*)$/;
755
+ /** Every call with a literal name. */
756
+ const CALLS = /\b(?:ctx|views)\.render\(\s*(['"`])([^'"`\n]*)\1/g;
757
+ const FILE = 17;
758
+ /** Template names a page can render: everything but components. */
759
+ function renderable(ws) {
760
+ const dir = `${ws.componentsDir()}/`;
761
+ return ws.names().filter((n) => !n.startsWith(dir));
762
+ }
763
+ function renderCompletions(ws, text, offset) {
764
+ const m = OPEN_CALL.exec(text.slice(0, offset));
765
+ if (!m) return void 0;
766
+ const range = {
767
+ start: posOf(text, offset - m[2].length),
768
+ end: posOf(text, offset)
769
+ };
770
+ return renderable(ws).map((name) => ({
771
+ label: name,
772
+ kind: FILE,
773
+ textEdit: {
774
+ range,
775
+ newText: name
776
+ }
777
+ }));
778
+ }
779
+ /** Calls whose name is a plain literal (not a template string with `${…}`). */
780
+ function calls(text) {
781
+ return [...text.matchAll(CALLS)].filter((m) => !m[2].includes("${")).map((m) => ({
782
+ name: m[2],
783
+ offset: m.index + m[0].length - m[2].length - 1
784
+ }));
785
+ }
786
+ function renderDefinition(ws, text, offset) {
787
+ const call = calls(text).find((c) => offset >= c.offset && offset <= c.offset + c.name.length);
788
+ if (!call || !ws.astOf(call.name)) return null;
789
+ const top = {
790
+ start: {
791
+ line: 0,
792
+ character: 0
793
+ },
794
+ end: {
795
+ line: 0,
796
+ character: 0
797
+ }
798
+ };
799
+ return {
800
+ originSelectionRange: {
801
+ start: posOf(text, call.offset),
802
+ end: posOf(text, call.offset + call.name.length)
803
+ },
804
+ targetUri: ws.uriOf(call.name),
805
+ targetRange: top,
806
+ targetSelectionRange: top
807
+ };
808
+ }
809
+ function renderDiagnostics(ws, text) {
810
+ return calls(text).filter((c) => !ws.astOf(c.name)).map((c) => ({
811
+ range: {
812
+ start: posOf(text, c.offset),
813
+ end: posOf(text, c.offset + c.name.length)
814
+ },
815
+ severity: 2,
816
+ source: "kick",
817
+ message: `template '${c.name}' not found`
818
+ }));
819
+ }
820
+ //#endregion
681
821
  //#region src/plugin/diagnostics.ts
682
822
  /** The `.kick` file a root virtual-code document belongs to. */
683
823
  function sourceUri(context, uri) {
@@ -692,6 +832,7 @@ function createDiagnosticsPlugin(ws) {
692
832
  } },
693
833
  create(context) {
694
834
  return { provideDiagnostics(document) {
835
+ if (document.languageId === "kick-render") return renderDiagnostics(ws, document.getText());
695
836
  if (document.languageId !== "kick") return void 0;
696
837
  const uri = sourceUri(context, document.uri);
697
838
  ws.sync(uri, document.getText());
@@ -718,6 +859,421 @@ function formatEdits(view, text, file, tabSize) {
718
859
  }];
719
860
  }
720
861
  //#endregion
862
+ //#region src/plugin/tokens.ts
863
+ /**
864
+ * Semantic tokens for `.kick` templates, so every LSP editor highlights the
865
+ * template syntax, not only VS Code with its TextMate grammar. Found by a
866
+ * lexical scan, like the language plugin's: the buffer being typed in rarely
867
+ * parses.
868
+ */
869
+ /** The legend, in order: standard types, so themes colour them. */
870
+ const TOKEN_TYPES = [
871
+ "keyword",
872
+ "comment",
873
+ "operator",
874
+ "function",
875
+ "variable",
876
+ "property",
877
+ "string",
878
+ "number",
879
+ "class",
880
+ "parameter"
881
+ ];
882
+ const LITERALS = /* @__PURE__ */ new Set([
883
+ "true",
884
+ "false",
885
+ "null",
886
+ "undefined"
887
+ ]);
888
+ /** Words with a meaning in block headers: `@for (x of xs; track x.id; let i = $index)`, `@if (a; as b)`, `@layout(none)`. */
889
+ const HEADER_WORDS = /* @__PURE__ */ new Set([
890
+ "of",
891
+ "track",
892
+ "let",
893
+ "as",
894
+ "none"
895
+ ]);
896
+ function kickTokens(s, isComponent) {
897
+ const out = [];
898
+ const push = (offset, length, type) => {
899
+ if (length > 0) out.push({
900
+ offset,
901
+ length,
902
+ type
903
+ });
904
+ };
905
+ /** An expression in `[from, to)`. */
906
+ const expr = (from, to, header) => {
907
+ let prev = "";
908
+ for (let i = from; i < to;) {
909
+ const c = s[i];
910
+ if (/\s/.test(c)) {
911
+ i++;
912
+ continue;
913
+ }
914
+ if (c === "'" || c === "\"" || c === "`") {
915
+ let j = i + 1;
916
+ while (j < to && s[j] !== c) j += s[j] === "\\" ? 2 : 1;
917
+ j = Math.min(j + 1, to);
918
+ push(i, j - i, "string");
919
+ prev = "lit";
920
+ i = j;
921
+ continue;
922
+ }
923
+ const num = /^\d+(?:\.\d+)?/.exec(s.slice(i, Math.min(to, i + 32)));
924
+ if (num) {
925
+ push(i, num[0].length, "number");
926
+ prev = "lit";
927
+ i += num[0].length;
928
+ continue;
929
+ }
930
+ const word = /^[A-Za-z_$][\w$]*/.exec(s.slice(i, Math.min(to, i + 128)));
931
+ if (word) {
932
+ const w = word[0];
933
+ const type = prev === "." ? "property" : prev === "|" ? "function" : LITERALS.has(w) || header && HEADER_WORDS.has(w) ? "keyword" : "variable";
934
+ push(i, w.length, type);
935
+ prev = "word";
936
+ i += w.length;
937
+ continue;
938
+ }
939
+ if (c === "|" && s[i + 1] === "|") {
940
+ prev = "||";
941
+ i += 2;
942
+ continue;
943
+ }
944
+ if (c === "|") push(i, 1, "operator");
945
+ prev = c === "?" && s[i + 1] === "." ? "." : c;
946
+ i += c === "?" && s[i + 1] === "." ? 2 : 1;
947
+ }
948
+ };
949
+ /** `{{ … }}` from `i` (at `{{`); returns its end. */
950
+ const interp = (i, limit) => {
951
+ const end = Math.min(closeInterp(s, i), limit);
952
+ const closed = s.startsWith("}}", end - 2) && end - 2 >= i + 2;
953
+ push(i, 2, "operator");
954
+ expr(i + 2, closed ? end - 2 : end, false);
955
+ if (closed) push(end - 2, 2, "operator");
956
+ return end;
957
+ };
958
+ /** One tag from `i` (at `<`); returns where scanning continues. */
959
+ const tag = (i) => {
960
+ const m = /^<(\/?)([A-Za-z][\w:.-]*)/.exec(s.slice(i, i + 64));
961
+ const name = m[2].toLowerCase();
962
+ if (isComponent(name)) push(i + 1 + m[1].length, m[2].length, "class");
963
+ let j = i + m[0].length;
964
+ while (j < s.length && s[j] !== ">") {
965
+ const binding = /^\[([^\]\s]+)\]\s*=\s*(["'])/.exec(s.slice(j, j + 128));
966
+ if (binding) {
967
+ push(j + 1, binding[1].length, "property");
968
+ const from = j + binding[0].length;
969
+ const close = s.indexOf(binding[2], from);
970
+ const stop = close < 0 ? s.length : close;
971
+ expr(from, stop, false);
972
+ j = stop + 1;
973
+ continue;
974
+ }
975
+ const q = s[j];
976
+ if (q === "\"" || q === "'") {
977
+ const close = s.indexOf(q, j + 1);
978
+ const stop = close < 0 ? s.length : close;
979
+ for (let k = j + 1; k < stop; k++) if (s.startsWith("{{", k)) k = interp(k, stop) - 1;
980
+ j = stop + 1;
981
+ continue;
982
+ }
983
+ j++;
984
+ }
985
+ j = Math.min(j + 1, s.length);
986
+ if (m[1] || name !== "script" && name !== "style" || s[j - 2] === "/") return j;
987
+ const close = new RegExp(`</${name}(?=[\\t\\n\\f\\r />])`, "i").exec(s.slice(j));
988
+ const end = close ? j + close.index : s.length;
989
+ for (let k = j; k < end; k++) if (s.startsWith("{{", k)) k = interp(k, end) - 1;
990
+ return end;
991
+ };
992
+ let i = 0;
993
+ while (i < s.length) {
994
+ const c = s[i];
995
+ if (s.startsWith("{{--", i)) {
996
+ const end = s.indexOf("--}}", i + 4);
997
+ const stop = end < 0 ? s.length : end + 4;
998
+ push(i, stop - i, "comment");
999
+ i = stop;
1000
+ continue;
1001
+ }
1002
+ if (s.startsWith("{{", i)) {
1003
+ i = interp(i, s.length);
1004
+ continue;
1005
+ }
1006
+ if (s.startsWith("<!--", i)) {
1007
+ const end = s.indexOf("-->", i + 4);
1008
+ i = end < 0 ? s.length : end + 3;
1009
+ continue;
1010
+ }
1011
+ if (c === "<" && /^<\/?[A-Za-z]/.test(s.slice(i, i + 3))) {
1012
+ i = tag(i);
1013
+ continue;
1014
+ }
1015
+ if (c === "@" && !/[\w@.-]/.test(s[i - 1] ?? "")) {
1016
+ const m = KEYWORD$1.exec(s.slice(i, i + 16));
1017
+ if (m) {
1018
+ const kw = m[1].replace(/\s+/g, " ");
1019
+ push(i, m[0].length, "keyword");
1020
+ let j = i + m[0].length;
1021
+ if (kw === "input" || kw === "let") {
1022
+ const decl = /^\s+([\w$]+)/.exec(s.slice(j, j + 64));
1023
+ if (decl) push(j + decl[0].length - decl[1].length, decl[1].length, kw === "input" ? "parameter" : "variable");
1024
+ const eol = s.indexOf("\n", j);
1025
+ const stop = kw === "let" ? letEnd(s, j).at : eol < 0 ? s.length : eol;
1026
+ const eq = s.indexOf("=", j);
1027
+ if (eq >= 0 && eq < stop) expr(eq + 1, stop, false);
1028
+ i = stop;
1029
+ continue;
1030
+ }
1031
+ while (j < s.length && /\s/.test(s[j])) j++;
1032
+ if (kw === "verbatim" && s[j] === "{") {
1033
+ i = verbatimEnd(s, j);
1034
+ continue;
1035
+ }
1036
+ if (s[j] === "(") {
1037
+ const end = closeParen$1(s, j);
1038
+ expr(j + 1, s[end - 1] === ")" ? end - 1 : end, true);
1039
+ i = end;
1040
+ continue;
1041
+ }
1042
+ i = i + m[0].length;
1043
+ continue;
1044
+ }
1045
+ }
1046
+ i++;
1047
+ }
1048
+ return out;
1049
+ }
1050
+ //#endregion
1051
+ //#region src/plugin/references.ts
1052
+ const TOP = {
1053
+ start: {
1054
+ line: 0,
1055
+ character: 0
1056
+ },
1057
+ end: {
1058
+ line: 0,
1059
+ character: 0
1060
+ }
1061
+ };
1062
+ const IDENT = /^[A-Za-z_$][\w$]*$/;
1063
+ const LAYOUT = /@layout\s*\(\s*(['"])([^'"]*)\1/g;
1064
+ const rangeOf = (text, offset, length) => ({
1065
+ start: posOf(text, offset),
1066
+ end: posOf(text, offset + length)
1067
+ });
1068
+ /** The shared name under the cursor, with where it sits in `text`. */
1069
+ function targetAt(ws, text, offset) {
1070
+ const t = kickTokens(text, (tag) => componentOf(ws, tag) !== void 0).find((k) => offset >= k.offset && offset <= k.offset + k.length);
1071
+ if (!t) return void 0;
1072
+ const word = text.slice(t.offset, t.offset + t.length);
1073
+ if (t.type === "class") {
1074
+ const name = componentOf(ws, word.toLowerCase());
1075
+ return name === void 0 ? void 0 : {
1076
+ target: {
1077
+ kind: "component",
1078
+ name,
1079
+ tag: word.toLowerCase()
1080
+ },
1081
+ ...t
1082
+ };
1083
+ }
1084
+ if (t.type === "function") return {
1085
+ target: {
1086
+ kind: "pipe",
1087
+ name: word
1088
+ },
1089
+ ...t
1090
+ };
1091
+ if (t.type === "string" && /@layout\s*\(\s*$/.test(text.slice(0, t.offset))) {
1092
+ const name = word.slice(1, -1);
1093
+ return ws.astOf(name) ? {
1094
+ target: {
1095
+ kind: "layout",
1096
+ name
1097
+ },
1098
+ offset: t.offset + 1,
1099
+ length: name.length
1100
+ } : void 0;
1101
+ }
1102
+ }
1103
+ /** Every use of `target` in the templates. */
1104
+ function usesOf(ws, target) {
1105
+ const out = [];
1106
+ for (const name of ws.names()) {
1107
+ const text = ws.textOf(name);
1108
+ if (text === void 0) continue;
1109
+ const uri = ws.uriOf(name);
1110
+ if (target.kind === "layout") {
1111
+ for (const m of text.matchAll(LAYOUT)) if (m[2] === target.name) out.push({
1112
+ uri,
1113
+ text,
1114
+ offset: m.index + m[0].length - m[2].length - 1,
1115
+ length: m[2].length
1116
+ });
1117
+ continue;
1118
+ }
1119
+ const want = target.kind === "component" ? "class" : "function";
1120
+ const match = target.kind === "component" ? target.tag : target.name;
1121
+ 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({
1122
+ uri,
1123
+ text,
1124
+ ...t
1125
+ });
1126
+ }
1127
+ return out;
1128
+ }
1129
+ /** Where a pipe is declared in `pipes.ts`; only the forms the server can read. */
1130
+ function pipeDeclaration(ws, name) {
1131
+ const decl = ws.pipeDeclaration(name);
1132
+ if (!decl) return void 0;
1133
+ const start = {
1134
+ line: decl.line,
1135
+ character: decl.column
1136
+ };
1137
+ return {
1138
+ uri: URI.file(ws.pipesFile()).toString(),
1139
+ range: {
1140
+ start,
1141
+ end: {
1142
+ line: decl.line,
1143
+ character: decl.column + name.length
1144
+ }
1145
+ }
1146
+ };
1147
+ }
1148
+ function references(ws, text, offset, includeDeclaration) {
1149
+ const at = targetAt(ws, text, offset);
1150
+ if (!at) return [];
1151
+ const { target } = at;
1152
+ const locations = usesOf(ws, target).map((u) => ({
1153
+ uri: u.uri,
1154
+ range: rangeOf(u.text, u.offset, u.length)
1155
+ }));
1156
+ if (includeDeclaration) {
1157
+ if (target.kind === "pipe") {
1158
+ const decl = pipeDeclaration(ws, target.name);
1159
+ if (decl) locations.push(decl);
1160
+ } else locations.push({
1161
+ uri: ws.uriOf(target.name),
1162
+ range: TOP
1163
+ });
1164
+ }
1165
+ return locations;
1166
+ }
1167
+ /** What F2 renames here: a pipe, a component tag or a layout name. */
1168
+ function renameRange(ws, text, offset) {
1169
+ const at = targetAt(ws, text, offset);
1170
+ if (!at) return void 0;
1171
+ 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.` };
1172
+ return rangeOf(text, at.offset, at.length);
1173
+ }
1174
+ const TAG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
1175
+ /** The component file for `tag`, kept in the old one's folder when the tag still starts with it. */
1176
+ function componentNameFor(ws, oldName, tag) {
1177
+ if (!TAG.test(tag)) return void 0;
1178
+ const dir = ws.componentsDir();
1179
+ const folders = oldName.slice(dir.length + 1).split("/").slice(0, -1);
1180
+ const prefix = folders.join("-");
1181
+ const name = prefix && tag.startsWith(`${prefix}-`) ? `${dir}/${folders.join("/")}/${tag.slice(prefix.length + 1)}` : `${dir}/${tag}`;
1182
+ return ws.view.componentTag(name, dir) === tag ? name : void 0;
1183
+ }
1184
+ /** Every use of `target` replaced with `newText`, then the template's file renamed. */
1185
+ function renameTemplate(ws, target, newText, newName) {
1186
+ if (target.kind === "pipe") return void 0;
1187
+ if (ws.view.templateName(newName) !== newName || ws.astOf(newName)) return void 0;
1188
+ const byUri = /* @__PURE__ */ new Map();
1189
+ for (const u of usesOf(ws, target)) {
1190
+ const edits = byUri.get(u.uri) ?? [];
1191
+ edits.push({
1192
+ range: rangeOf(u.text, u.offset, u.length),
1193
+ newText
1194
+ });
1195
+ byUri.set(u.uri, edits);
1196
+ }
1197
+ return { documentChanges: [...[...byUri].map(([uri, edits]) => ({
1198
+ textDocument: {
1199
+ uri,
1200
+ version: null
1201
+ },
1202
+ edits
1203
+ })), {
1204
+ kind: "rename",
1205
+ oldUri: ws.uriOf(target.name),
1206
+ newUri: ws.uriOf(newName)
1207
+ }] };
1208
+ }
1209
+ /** A pipe renamed in every template and in `pipes.ts`; a component or layout, file included. */
1210
+ function renameEdits(ws, text, offset, newName) {
1211
+ const at = targetAt(ws, text, offset);
1212
+ if (!at) return void 0;
1213
+ const { target } = at;
1214
+ if (target.kind === "component") {
1215
+ const name = componentNameFor(ws, target.name, newName);
1216
+ return name === void 0 ? void 0 : renameTemplate(ws, target, newName, name);
1217
+ }
1218
+ if (target.kind === "layout") return renameTemplate(ws, target, newName, newName);
1219
+ if (!IDENT.test(newName) || ws.project().pipes.has(newName)) return void 0;
1220
+ const decl = pipeDeclaration(ws, target.name);
1221
+ if (!decl) return void 0;
1222
+ const changes = {};
1223
+ const add = (uri, range) => (changes[uri] ??= []).push({
1224
+ range,
1225
+ newText: newName
1226
+ });
1227
+ for (const u of usesOf(ws, target)) add(u.uri, rangeOf(u.text, u.offset, u.length));
1228
+ add(decl.uri, decl.range);
1229
+ return { changes };
1230
+ }
1231
+ /**
1232
+ * Edits that keep uses pointing at renamed templates: a component's tags, a
1233
+ * layout's `@layout('…')`, a page's `ctx.render('…')`. `oldUri` / `newUri`
1234
+ * can be a file or a folder.
1235
+ */
1236
+ async function fileRenameEdits(ws, oldUri, newUri) {
1237
+ const from = ws.relOf(oldUri)?.replace(/\.kick$/, "");
1238
+ const to = ws.relOf(newUri)?.replace(/\.kick$/, "");
1239
+ if (from === void 0 || to === void 0 || from === to) return void 0;
1240
+ const renamed = /* @__PURE__ */ new Map();
1241
+ for (const name of ws.names()) if (name === from) renamed.set(name, to);
1242
+ else if (name.startsWith(`${from}/`)) renamed.set(name, to + name.slice(from.length));
1243
+ if (!renamed.size) return void 0;
1244
+ const changes = {};
1245
+ const dir = ws.componentsDir();
1246
+ for (const [oldName, newName] of renamed) {
1247
+ const comp = oldName.startsWith(`${dir}/`);
1248
+ const newTag = newName.startsWith(`${dir}/`) ? ws.view.componentTag(newName, dir) : void 0;
1249
+ const targets = [[{
1250
+ kind: "layout",
1251
+ name: oldName
1252
+ }, newName]];
1253
+ if (comp && newTag) targets.push([{
1254
+ kind: "component",
1255
+ name: oldName,
1256
+ tag: ws.view.componentTag(oldName, dir)
1257
+ }, newTag]);
1258
+ for (const [target, newText] of targets) for (const u of usesOf(ws, target)) (changes[u.uri] ??= []).push({
1259
+ range: rangeOf(u.text, u.offset, u.length),
1260
+ newText
1261
+ });
1262
+ }
1263
+ const pages = [...renamed].filter(([n]) => !n.startsWith(`${dir}/`));
1264
+ if (pages.length) {
1265
+ const to = new Map(pages);
1266
+ for (const script of await ws.scripts()) for (const c of calls(script.text)) {
1267
+ const newName = to.get(c.name);
1268
+ if (newName !== void 0) (changes[script.uri] ??= []).push({
1269
+ range: rangeOf(script.text, c.offset, c.name.length),
1270
+ newText: newName
1271
+ });
1272
+ }
1273
+ }
1274
+ return Object.keys(changes).length ? { changes } : void 0;
1275
+ }
1276
+ //#endregion
721
1277
  //#region src/plugin/index.ts
722
1278
  function createKickServicePlugin(ws) {
723
1279
  const diagnostics = createDiagnosticsPlugin(ws);
@@ -735,7 +1291,14 @@ function createKickServicePlugin(ws) {
735
1291
  ] },
736
1292
  hoverProvider: true,
737
1293
  definitionProvider: true,
738
- documentFormattingProvider: true
1294
+ documentFormattingProvider: true,
1295
+ semanticTokensProvider: { legend: {
1296
+ tokenTypes: [...TOKEN_TYPES],
1297
+ tokenModifiers: []
1298
+ } },
1299
+ referencesProvider: true,
1300
+ renameProvider: { prepareProvider: true },
1301
+ fileRenameEditsProvider: true
739
1302
  },
740
1303
  create(context) {
741
1304
  /** The source file's uri and text, with the workspace synced to the buffer. */
@@ -752,6 +1315,13 @@ function createKickServicePlugin(ws) {
752
1315
  return {
753
1316
  ...diagnostics.create(context),
754
1317
  provideCompletionItems(document, position) {
1318
+ if (document.languageId === "kick-render") {
1319
+ const items = renderCompletions(ws, document.getText(), document.offsetAt(position));
1320
+ return items && {
1321
+ isIncomplete: false,
1322
+ items
1323
+ };
1324
+ }
755
1325
  const d = open(document);
756
1326
  if (!d) return void 0;
757
1327
  const items = completions(ws, d.uri, d.text, document.offsetAt(position));
@@ -765,10 +1335,51 @@ function createKickServicePlugin(ws) {
765
1335
  return d ? hover(ws, d.uri, d.text, document.offsetAt(position)) ?? void 0 : void 0;
766
1336
  },
767
1337
  provideDefinition(document, position) {
1338
+ if (document.languageId === "kick-render") {
1339
+ const link = renderDefinition(ws, document.getText(), document.offsetAt(position));
1340
+ return link ? [link] : void 0;
1341
+ }
768
1342
  const d = open(document);
769
1343
  const link = d && definition(ws, d.uri, d.text, document.offsetAt(position));
770
1344
  return link ? [link] : void 0;
771
1345
  },
1346
+ provideReferences(document, position, context) {
1347
+ const d = open(document);
1348
+ return d ? references(ws, d.text, document.offsetAt(position), context.includeDeclaration) : void 0;
1349
+ },
1350
+ provideRenameRange(document, position) {
1351
+ const d = open(document);
1352
+ return d ? renameRange(ws, d.text, document.offsetAt(position)) : void 0;
1353
+ },
1354
+ provideRenameEdits(document, position, newName) {
1355
+ const d = open(document);
1356
+ return d ? renameEdits(ws, d.text, document.offsetAt(position), newName) : void 0;
1357
+ },
1358
+ provideFileRenameEdits(oldUri, newUri) {
1359
+ return fileRenameEdits(ws, oldUri.toString(), newUri.toString());
1360
+ },
1361
+ provideDocumentSemanticTokens(document, range, legend) {
1362
+ const d = open(document);
1363
+ if (!d) return void 0;
1364
+ const tokens = [];
1365
+ for (const t of kickTokens(d.text, (tag) => componentOf(ws, tag) !== void 0)) {
1366
+ const type = legend.tokenTypes.indexOf(t.type);
1367
+ if (type < 0) continue;
1368
+ let at = t.offset;
1369
+ for (const part of d.text.slice(t.offset, t.offset + t.length).split("\n")) {
1370
+ const p = document.positionAt(at);
1371
+ if (part && p.line >= range.start.line && p.line <= range.end.line) tokens.push([
1372
+ p.line,
1373
+ p.character,
1374
+ part.length,
1375
+ type,
1376
+ 0
1377
+ ]);
1378
+ at += part.length + 1;
1379
+ }
1380
+ }
1381
+ return tokens;
1382
+ },
772
1383
  provideDocumentFormattingEdits(document, _range, options) {
773
1384
  const d = open(document);
774
1385
  if (!d) return void 0;
@@ -952,6 +1563,11 @@ var Workspace = class {
952
1563
  if (rel.startsWith("..") || !rel.endsWith(".kick")) return void 0;
953
1564
  return this.opts.view.templateName(rel.slice(0, -5));
954
1565
  }
1566
+ /** `uri`'s path inside the views folder (`layouts/main.kick`, `layouts`); `undefined` outside it. */
1567
+ relOf(uri) {
1568
+ const rel = relative(this.opts.viewsDir, URI.parse(uri).fsPath).split(sep).join(posix.sep);
1569
+ return rel.startsWith("..") || isAbsolute(rel) ? void 0 : rel;
1570
+ }
955
1571
  nameOf(uri) {
956
1572
  return this.nameOfPath(URI.parse(uri).fsPath);
957
1573
  }
@@ -1069,6 +1685,7 @@ var Workspace = class {
1069
1685
  }
1070
1686
  /** Templates that use `name` as a component or a layout — a layout even after it's gone. */
1071
1687
  dependentsOf(name) {
1688
+ if (this.configuredLayouts().has(name)) return new Set(this.names().filter((n) => n !== name));
1072
1689
  const out = /* @__PURE__ */ new Set();
1073
1690
  const tag = name.startsWith(`${this.opts.componentsDir}/`) ? this.opts.view.componentTag(name, this.opts.componentsDir) : void 0;
1074
1691
  for (const [n, e] of this.entries) {
@@ -1091,9 +1708,38 @@ var Workspace = class {
1091
1708
  return {
1092
1709
  templates: new Map([...this.entries].map(([n, e]) => [n, e.ast])),
1093
1710
  pipes: new Set(this.pipeNames()),
1094
- componentsDir: this.opts.componentsDir
1711
+ componentsDir: this.opts.componentsDir,
1712
+ layouts: this.opts.layout,
1713
+ baseLayout: this.opts.baseLayout
1095
1714
  };
1096
1715
  }
1716
+ /** Templates that are layouts by configuration: the mapping's targets and the base layout. */
1717
+ configuredLayouts() {
1718
+ const { layout, baseLayout } = this.opts;
1719
+ const out = new Set(typeof layout === "string" ? [layout] : Object.values(layout ?? {}));
1720
+ if (baseLayout) out.add(baseLayout);
1721
+ return out;
1722
+ }
1723
+ scripts() {
1724
+ return this.opts.scripts?.() ?? Promise.resolve([]);
1725
+ }
1726
+ baseLayout() {
1727
+ return this.opts.baseLayout;
1728
+ }
1729
+ /**
1730
+ * The layout `name` renders in: its own `@layout`, else the configured
1731
+ * mapping's longest matching folder prefix (pages only, as `check` does).
1732
+ */
1733
+ layoutOf(name) {
1734
+ const ast = this.astOf(name);
1735
+ if (!ast) return void 0;
1736
+ if (ast.layout !== void 0) return ast.layout ?? void 0;
1737
+ const { layout } = this.opts;
1738
+ if (layout === void 0 || name.startsWith(`${this.opts.componentsDir}/`) || this.configuredLayouts().has(name)) return void 0;
1739
+ if (typeof layout === "string") return layout;
1740
+ const hit = Object.keys(layout).toSorted((a, b) => b.length - a.length).find((p) => p === "" || name === p || name.startsWith(`${p}/`));
1741
+ return hit === void 0 ? void 0 : layout[hit];
1742
+ }
1097
1743
  /** Parse errors, else everything `check` reports — as LSP diagnostics. */
1098
1744
  diagnosticsFor(uri) {
1099
1745
  const name = this.nameOf(uri);
@@ -1126,6 +1772,47 @@ async function listKick(dir) {
1126
1772
  return [];
1127
1773
  }
1128
1774
  }
1775
+ const SKIP = /* @__PURE__ */ new Set([
1776
+ "node_modules",
1777
+ "dist",
1778
+ "build",
1779
+ "coverage",
1780
+ "out"
1781
+ ]);
1782
+ const SCRIPT = /\.(?:[cm]?[jt]s|[jt]sx)$/;
1783
+ /** TS / JS files under `root`, for `ctx.render('…')` names: no dependencies, build output, dot folders or `.d.ts`. */
1784
+ async function listScripts(root) {
1785
+ const out = [];
1786
+ const walk = async (dir) => {
1787
+ let entries;
1788
+ try {
1789
+ entries = await readdir(dir, { withFileTypes: true });
1790
+ } catch {
1791
+ return;
1792
+ }
1793
+ for (const e of entries) {
1794
+ if (e.name.startsWith(".") || SKIP.has(e.name)) continue;
1795
+ const p = join(dir, e.name);
1796
+ if (e.isDirectory()) await walk(p);
1797
+ else if (SCRIPT.test(e.name) && !e.name.endsWith(".d.ts")) out.push(p);
1798
+ }
1799
+ };
1800
+ await walk(root);
1801
+ return out;
1802
+ }
1803
+ /** `fn` over `items` with at most `limit` running at once, results in order. */
1804
+ async function mapLimit(items, limit, fn) {
1805
+ const out = Array.from({ length: items.length });
1806
+ let next = 0;
1807
+ const worker = async () => {
1808
+ while (next < items.length) {
1809
+ const i = next++;
1810
+ out[i] = await fn(items[i]);
1811
+ }
1812
+ };
1813
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
1814
+ return out;
1815
+ }
1129
1816
  /** What the server watches: every template, and the pipes file wherever it's configured. */
1130
1817
  function watchPatterns(root, pipesFile) {
1131
1818
  const patterns = ["**/*.kick", "**/pipes.{ts,js}"];
@@ -1155,14 +1842,23 @@ function startServer() {
1155
1842
  pipesFile,
1156
1843
  view,
1157
1844
  read: (p) => readFile(p, "utf8").catch(() => void 0),
1158
- list: listKick
1845
+ list: listKick,
1846
+ layout: opts.layout || void 0,
1847
+ baseLayout: opts.baseLayout || void 0,
1848
+ scripts: async () => mapLimit(await listScripts(root), 64, async (p) => {
1849
+ const uri = URI.file(p).toString();
1850
+ return {
1851
+ uri,
1852
+ text: server.documents.get(URI.parse(uri))?.getText() ?? await readFile(p, "utf8").catch(() => "")
1853
+ };
1854
+ })
1159
1855
  });
1160
1856
  await ws.load();
1161
1857
  return server.initialize(params, createSimpleProject([kickLanguagePlugin]), [
1858
+ createKickServicePlugin(ws),
1162
1859
  create$2(),
1163
1860
  create(),
1164
- create$1(),
1165
- createKickServicePlugin(ws)
1861
+ create$1()
1166
1862
  ]);
1167
1863
  });
1168
1864
  connection.onInitialized(() => {
@@ -1179,7 +1875,7 @@ function startServer() {
1179
1875
  await server.languageFeatures.requestRefresh(false);
1180
1876
  });
1181
1877
  server.documents.onDidChangeContent(({ document }) => {
1182
- if (document.languageId === "kick") ws?.sync(document.uri, document.getText());
1878
+ if (document.uri.endsWith(".kick")) ws?.sync(document.uri, document.getText());
1183
1879
  });
1184
1880
  server.documents.onDidClose(async ({ document }) => {
1185
1881
  if (!ws) return;
@@ -1191,9 +1887,17 @@ function startServer() {
1191
1887
  const dir = `${ws.componentsDir()}/`;
1192
1888
  return ws.names().filter((n) => !n.startsWith(dir));
1193
1889
  });
1890
+ connection.onRequest("kick/renderDefinition", ({ text, offset }) => {
1891
+ const link = ws && renderDefinition(ws, text, offset);
1892
+ return link ? {
1893
+ uri: link.targetUri,
1894
+ range: link.originSelectionRange
1895
+ } : null;
1896
+ });
1897
+ connection.onRequest("kick/renderDiagnostics", ({ text }) => ws ? renderDiagnostics(ws, text) : []);
1194
1898
  connection.onShutdown(() => server.shutdown());
1195
1899
  }
1196
1900
  //#endregion
1197
1901
  export { KickVirtualCode as a, createKickServicePlugin as i, Workspace as n, kickLanguagePlugin as o, loadViewModule as r, startServer as t };
1198
1902
 
1199
- //# sourceMappingURL=server-Dak0ereo.js.map
1903
+ //# sourceMappingURL=server-pe6A1Wn-.js.map