@monimesl/dot-cli 0.1.0-alpha.4 → 0.1.0-alpha.40

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.
Files changed (161) hide show
  1. package/README.md +84 -29
  2. package/dist/cli.js +686 -319
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-DFOUQnLA.js → server-DMnWDdJ6.js} +582 -537
  5. package/dist/server-DMnWDdJ6.js.map +1 -0
  6. package/dist/templates/showcase/.env +1 -1
  7. package/dist/templates/showcase/dot.ts +53 -14
  8. package/dist/templates/showcase/pages/basics/bars/controller.ts +47 -0
  9. package/dist/templates/showcase/pages/basics/bars/state.ts +7 -0
  10. package/dist/templates/showcase/pages/basics/bars/view.dml +36 -0
  11. package/dist/templates/showcase/pages/basics/bindings/controller.ts +15 -0
  12. package/dist/templates/showcase/pages/basics/bindings/state.ts +14 -0
  13. package/dist/templates/showcase/pages/basics/bindings/view.dml +41 -0
  14. package/dist/templates/showcase/pages/basics/colors/controller.ts +112 -0
  15. package/dist/templates/showcase/pages/basics/colors/palette.ts +52 -0
  16. package/dist/templates/showcase/pages/basics/colors/state.ts +17 -0
  17. package/dist/templates/showcase/pages/basics/colors/view.dml +142 -0
  18. package/dist/templates/showcase/pages/basics/concert/controller.ts +16 -0
  19. package/dist/templates/showcase/pages/basics/concert/state.ts +7 -0
  20. package/dist/templates/showcase/pages/basics/concert/view.dml +20 -0
  21. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +18 -0
  22. package/dist/templates/showcase/pages/basics/control-flow/state.ts +16 -0
  23. package/dist/templates/showcase/pages/basics/control-flow/view.dml +55 -0
  24. package/dist/templates/showcase/pages/basics/events/controller.ts +43 -0
  25. package/dist/templates/showcase/pages/basics/events/state.ts +11 -0
  26. package/dist/templates/showcase/pages/basics/events/view.dml +51 -0
  27. package/dist/templates/showcase/pages/basics/hooks/controller.ts +43 -0
  28. package/dist/templates/showcase/pages/basics/hooks/state.ts +10 -0
  29. package/dist/templates/showcase/pages/basics/hooks/view.dml +37 -0
  30. package/dist/templates/showcase/pages/basics/navigation/controller.ts +41 -0
  31. package/dist/templates/showcase/pages/basics/navigation/state.ts +11 -0
  32. package/dist/templates/showcase/pages/basics/navigation/view.dml +48 -0
  33. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +18 -0
  34. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  35. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +10 -0
  36. package/dist/templates/showcase/pages/basics/share/concerts.ts +31 -0
  37. package/dist/templates/showcase/pages/basics/share/controller.ts +12 -0
  38. package/dist/templates/showcase/pages/basics/share/state.ts +5 -0
  39. package/dist/templates/showcase/pages/basics/share/view.dml +21 -0
  40. package/dist/templates/showcase/pages/{state → basics/state}/controller.ts +1 -0
  41. package/dist/templates/showcase/pages/{state → basics/state}/state.ts +1 -1
  42. package/dist/templates/showcase/pages/basics/state/view.dml +46 -0
  43. package/dist/templates/showcase/pages/components/carousel/controller.ts +41 -0
  44. package/dist/templates/showcase/pages/components/carousel/state.ts +77 -0
  45. package/dist/templates/showcase/pages/components/carousel/view.dml +42 -0
  46. package/dist/templates/showcase/pages/components/feedback/controller.ts +32 -0
  47. package/dist/templates/showcase/pages/components/feedback/state.ts +41 -0
  48. package/dist/templates/showcase/pages/components/feedback/view.dml +116 -0
  49. package/dist/templates/showcase/pages/components/forms/controller.ts +72 -0
  50. package/dist/templates/showcase/pages/components/forms/state.ts +37 -0
  51. package/dist/templates/showcase/pages/components/forms/view.dml +97 -0
  52. package/dist/templates/showcase/pages/components/icons/controller.ts +15 -0
  53. package/dist/templates/showcase/pages/components/icons/state.ts +75 -0
  54. package/dist/templates/showcase/pages/components/icons/view.dml +36 -0
  55. package/dist/templates/showcase/pages/components/images/controller.ts +44 -0
  56. package/dist/templates/showcase/pages/{images → components/images}/state.ts +3 -0
  57. package/dist/templates/showcase/pages/components/images/view.dml +83 -0
  58. package/dist/templates/showcase/pages/components/layout/controller.ts +61 -0
  59. package/dist/templates/showcase/pages/components/layout/state.ts +34 -0
  60. package/dist/templates/showcase/pages/components/layout/view.dml +143 -0
  61. package/dist/templates/showcase/pages/components/lists/controller.ts +18 -0
  62. package/dist/templates/showcase/pages/components/lists/state.ts +130 -0
  63. package/dist/templates/showcase/pages/components/lists/view.dml +88 -0
  64. package/dist/templates/showcase/pages/components/money/controller.ts +45 -0
  65. package/dist/templates/showcase/pages/{payments → components/money}/state.ts +3 -0
  66. package/dist/templates/showcase/pages/components/money/view.dml +54 -0
  67. package/dist/templates/showcase/pages/components/reading/controller.ts +6 -0
  68. package/dist/templates/showcase/pages/components/reading/state.ts +23 -0
  69. package/dist/templates/showcase/pages/components/reading/view.dml +42 -0
  70. package/dist/templates/showcase/pages/components/sheets/controller.ts +36 -0
  71. package/dist/templates/showcase/pages/components/sheets/state.ts +26 -0
  72. package/dist/templates/showcase/pages/components/sheets/view.dml +38 -0
  73. package/dist/templates/showcase/pages/components/styles/controller.ts +6 -0
  74. package/dist/templates/showcase/pages/components/styles/state.ts +3 -0
  75. package/dist/templates/showcase/pages/components/styles/view.dml +101 -0
  76. package/dist/templates/showcase/pages/components/tabs/controller.ts +41 -0
  77. package/dist/templates/showcase/pages/components/tabs/state.ts +127 -0
  78. package/dist/templates/showcase/pages/components/tabs/view.dml +57 -0
  79. package/dist/templates/showcase/pages/components/text/controller.ts +10 -0
  80. package/dist/templates/showcase/pages/components/text/state.ts +3 -0
  81. package/dist/templates/showcase/pages/components/text/view.dml +95 -0
  82. package/dist/templates/showcase/pages/dash/account/controller.ts +32 -0
  83. package/dist/templates/showcase/pages/dash/account/state.ts +9 -0
  84. package/dist/templates/showcase/pages/dash/account/view.dml +33 -0
  85. package/dist/templates/showcase/pages/dash/cache/controller.ts +50 -0
  86. package/dist/templates/showcase/pages/dash/cache/state.ts +17 -0
  87. package/dist/templates/showcase/pages/dash/cache/view.dml +48 -0
  88. package/dist/templates/showcase/pages/dash/calls/controller.ts +67 -0
  89. package/dist/templates/showcase/pages/dash/calls/state.ts +9 -0
  90. package/dist/templates/showcase/pages/dash/calls/view.dml +69 -0
  91. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  92. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  93. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  94. package/dist/templates/showcase/pages/dash/crypto/controller.ts +38 -0
  95. package/dist/templates/showcase/pages/dash/crypto/state.ts +10 -0
  96. package/dist/templates/showcase/pages/dash/crypto/view.dml +48 -0
  97. package/dist/templates/showcase/pages/dash/files/controller.ts +71 -0
  98. package/dist/templates/showcase/pages/dash/files/state.ts +23 -0
  99. package/dist/templates/showcase/pages/dash/files/view.dml +78 -0
  100. package/dist/templates/showcase/pages/dash/media/controller.ts +65 -0
  101. package/dist/templates/showcase/pages/dash/media/state.ts +12 -0
  102. package/dist/templates/showcase/pages/dash/media/view.dml +53 -0
  103. package/dist/templates/showcase/pages/dash/network/controller.ts +215 -0
  104. package/dist/templates/showcase/pages/dash/network/state.ts +35 -0
  105. package/dist/templates/showcase/pages/dash/network/view.dml +108 -0
  106. package/dist/templates/showcase/pages/dash/payments/controller.ts +83 -0
  107. package/dist/templates/showcase/pages/dash/payments/state.ts +10 -0
  108. package/dist/templates/showcase/pages/dash/payments/view.dml +38 -0
  109. package/dist/templates/showcase/pages/dash/secrets/controller.ts +44 -0
  110. package/dist/templates/showcase/pages/dash/secrets/state.ts +5 -0
  111. package/dist/templates/showcase/pages/dash/secrets/view.dml +26 -0
  112. package/dist/templates/showcase/pages/dash/system/controller.ts +89 -0
  113. package/dist/templates/showcase/pages/dash/system/state.ts +13 -0
  114. package/dist/templates/showcase/pages/dash/system/view.dml +96 -0
  115. package/dist/templates/showcase/pages/dash/ui/controller.ts +70 -0
  116. package/dist/templates/showcase/pages/dash/ui/state.ts +8 -0
  117. package/dist/templates/showcase/pages/dash/ui/view.dml +31 -0
  118. package/dist/templates/showcase/pages/index/controller.ts +1 -1
  119. package/dist/templates/showcase/pages/index/state.ts +181 -54
  120. package/dist/templates/showcase/pages/index/view.dml +8 -5
  121. package/dist/templates/showcase/statics/logo.png +0 -0
  122. package/package.json +6 -6
  123. package/dist/server-DFOUQnLA.js.map +0 -1
  124. package/dist/templates/showcase/pages/carousel/controller.ts +0 -10
  125. package/dist/templates/showcase/pages/carousel/state.ts +0 -40
  126. package/dist/templates/showcase/pages/carousel/view.dml +0 -37
  127. package/dist/templates/showcase/pages/device/controller.ts +0 -70
  128. package/dist/templates/showcase/pages/device/state.ts +0 -7
  129. package/dist/templates/showcase/pages/device/view.dml +0 -44
  130. package/dist/templates/showcase/pages/feedback/controller.ts +0 -16
  131. package/dist/templates/showcase/pages/feedback/state.ts +0 -7
  132. package/dist/templates/showcase/pages/feedback/view.dml +0 -87
  133. package/dist/templates/showcase/pages/files/controller.ts +0 -47
  134. package/dist/templates/showcase/pages/files/state.ts +0 -11
  135. package/dist/templates/showcase/pages/files/view.dml +0 -35
  136. package/dist/templates/showcase/pages/filters/controller.ts +0 -28
  137. package/dist/templates/showcase/pages/filters/state.ts +0 -72
  138. package/dist/templates/showcase/pages/filters/view.dml +0 -43
  139. package/dist/templates/showcase/pages/forms/controller.ts +0 -44
  140. package/dist/templates/showcase/pages/forms/state.ts +0 -15
  141. package/dist/templates/showcase/pages/forms/view.dml +0 -39
  142. package/dist/templates/showcase/pages/images/controller.ts +0 -15
  143. package/dist/templates/showcase/pages/images/view.dml +0 -38
  144. package/dist/templates/showcase/pages/layout/controller.ts +0 -26
  145. package/dist/templates/showcase/pages/layout/state.ts +0 -14
  146. package/dist/templates/showcase/pages/layout/view.dml +0 -80
  147. package/dist/templates/showcase/pages/lists/controller.ts +0 -36
  148. package/dist/templates/showcase/pages/lists/state.ts +0 -96
  149. package/dist/templates/showcase/pages/lists/view.dml +0 -59
  150. package/dist/templates/showcase/pages/navigation/controller.ts +0 -64
  151. package/dist/templates/showcase/pages/navigation/state.ts +0 -17
  152. package/dist/templates/showcase/pages/navigation/view.dml +0 -34
  153. package/dist/templates/showcase/pages/network/controller.ts +0 -107
  154. package/dist/templates/showcase/pages/network/state.ts +0 -15
  155. package/dist/templates/showcase/pages/network/view.dml +0 -56
  156. package/dist/templates/showcase/pages/payments/controller.ts +0 -42
  157. package/dist/templates/showcase/pages/payments/view.dml +0 -35
  158. package/dist/templates/showcase/pages/state/view.dml +0 -33
  159. package/dist/templates/showcase/pages/text/controller.ts +0 -10
  160. package/dist/templates/showcase/pages/text/state.ts +0 -7
  161. package/dist/templates/showcase/pages/text/view.dml +0 -49
@@ -1,3 +1,4 @@
1
+ import { t as findDotRoot } from "./cli.js";
1
2
  import { createRequire } from "node:module";
2
3
  import { dirname, join } from "node:path";
3
4
  import { CONTROL_FLOW_TAGS, DATA_PREFIX, ROOT_TAG, canHaveChildren, compileView, dataKey, listStatics } from "@monimesl/dot-compiler";
@@ -5,7 +6,7 @@ import { existsSync } from "node:fs";
5
6
  import { readFile } from "node:fs/promises";
6
7
  import { parseDmlToIR } from "@monimesl/dot-dml-parser";
7
8
  import { ScriptParseError, parseExpression, parseLogicModule, parseStateModule } from "@monimesl/dot-script-parser";
8
- import { SourceError, builtinRegistry, eventName, eventProp, isEventProp } from "@monimesl/dot-core";
9
+ import { SourceError, builtinRegistry, eventName, eventProp, isEventProp, isWordsOr, otherThanWords } from "@monimesl/dot-core";
9
10
  import { fileURLToPath, pathToFileURL } from "node:url";
10
11
  import ts from "typescript";
11
12
  //#region \0rolldown/runtime.js
@@ -10143,165 +10144,57 @@ function getWellformedEdit(textEdit) {
10143
10144
  return textEdit;
10144
10145
  }
10145
10146
  //#endregion
10146
- //#region src/lsp/shapes.ts
10147
- const NUMBER = {
10148
- kind: "value",
10149
- type: "number"
10147
+ //#region src/lsp/descriptions.ts
10148
+ const CONTROL_FLOW_DOCS = {
10149
+ If: "Shows its children when `condition` is true.",
10150
+ ElseIf: "Goes after an `<If>` or `<ElseIf>`. Shows its children when every condition before it is false and `condition` is true.",
10151
+ Else: "Goes after an `<If>` or `<ElseIf>`. Shows its children when every condition before it is false.",
10152
+ For: "Repeats its children for each item in `each`. Inside, the item is named `item` and its position `index`. `key` tells items apart: a property of each item, `*this` for the item itself, or `*index` to match items by position."
10150
10153
  };
10151
- function parseBinding(source) {
10152
- try {
10153
- return parseExpression(source);
10154
- } catch (error) {
10155
- if (error instanceof ScriptParseError) return;
10156
- throw error;
10157
- }
10158
- }
10159
- function shapeOf(expr, scope) {
10160
- switch (expr.type) {
10161
- case "Identifier":
10162
- if (scope.loops.has(expr.name)) return scope.loops.get(expr.name);
10163
- return scope.state[expr.name];
10164
- case "Member": {
10165
- const owner = shapeOf(expr.object, scope);
10166
- if (!owner) return;
10167
- const { property } = expr;
10168
- if (property.type === "Literal" && typeof property.value === "string") return membersOf(owner)[property.value];
10169
- return owner.kind === "list" ? owner.item : void 0;
10170
- }
10171
- default: return;
10172
- }
10173
- }
10174
- function membersOf(shape) {
10175
- switch (shape.kind) {
10176
- case "object": return shape.properties;
10177
- case "list": return { length: NUMBER };
10178
- case "value": return {};
10154
+ const CONTROL_FLOW_ATTRIBUTE_DOCS = {
10155
+ condition: "A single `{{ }}` binding",
10156
+ each: "The list to repeat, as a single `{{ }}` binding",
10157
+ item: "The name for each item (default `item`)",
10158
+ index: "The name for each position (default `index`)",
10159
+ key: "A property of each item, `*this` or `*index`"
10160
+ };
10161
+ function describeComponent(component) {
10162
+ const lines = [`**<${component.name}>** · ${describeChildren(component)}`];
10163
+ const props = Object.entries(component.props);
10164
+ if (props.length > 0) lines.push("", ...props.map(([name, type]) => `- \`${name}\`: ${describeType(type)}`));
10165
+ if (component.events.length > 0) {
10166
+ const eventProps = component.events.map((event) => `\`${eventProp(event)}\``);
10167
+ lines.push("", `Events: ${eventProps.join(", ")}`);
10179
10168
  }
10169
+ return lines.join("\n");
10180
10170
  }
10181
- function describeMembers(shape) {
10182
- return Object.keys(membersOf(shape)).join(", ") || "none";
10183
- }
10184
- function loopScope(variables, state) {
10185
- const loops = /* @__PURE__ */ new Map();
10186
- for (const variable of variables) if (variable.role === "index") loops.set(variable.name, NUMBER);
10187
- else loops.set(variable.name, itemShape(variable.list, {
10188
- state,
10189
- loops
10190
- }));
10191
- return {
10192
- state,
10193
- loops
10194
- };
10195
- }
10196
- function itemShape(each, scope) {
10197
- const list = parseBinding(each.replace(/^\s*{{|}}\s*$/g, ""));
10198
- const shape = list && shapeOf(list, scope);
10199
- return shape?.kind === "list" ? shape.item : void 0;
10200
- }
10201
- function withLoop(scope, item, itemShape, index) {
10202
- const loops = new Map(scope.loops).set(item, itemShape).set(index, NUMBER);
10203
- return {
10204
- state: scope.state,
10205
- loops
10206
- };
10207
- }
10208
- //#endregion
10209
- //#region src/lsp/check-members.ts
10210
- const BINDING = /{{([\s\S]*?)}}/g;
10211
- /**
10212
- * Finds the bindings that read a property their value doesn't have, going by the types in state.ts:
10213
- * `{{ tap.nmae }}` where each tap is a `Tap`. A value whose type isn't known is not checked.
10214
- */
10215
- function checkMembers(text, state) {
10216
- let nodes;
10217
- try {
10218
- nodes = parseDmlToIR(text, {
10219
- locations: true,
10220
- whitespace: "preserve"
10221
- });
10222
- } catch {
10223
- return [];
10224
- }
10225
- const problems = [];
10226
- checkNodes(text, nodes, {
10227
- state,
10228
- loops: /* @__PURE__ */ new Map()
10229
- }, problems);
10230
- return problems;
10171
+ function describeType(type) {
10172
+ if (typeof type === "string") return type;
10173
+ if (isWordsOr(type)) return `${describeType(type.words)} | ${otherThanWords(type)}`;
10174
+ return type.map((value) => `"${value}"`).join(" | ");
10231
10175
  }
10232
- function checkNodes(text, nodes, scope, problems) {
10233
- for (const node of nodes) {
10234
- const from = node.loc?.offset ?? 0;
10235
- if (node.type === "Text") {
10236
- checkBindings(text, node.content, from, scope, problems);
10237
- continue;
10238
- }
10239
- for (const value of Object.values(node.attributes)) checkBindings(text, value, from, scope, problems);
10240
- if (node.tag !== "For") {
10241
- checkNodes(text, node.children, scope, problems);
10242
- continue;
10243
- }
10244
- const { each = "", item = "item", index = "index", key } = node.attributes;
10245
- const itemOfList = itemShape(each, scope);
10246
- if (itemOfList?.kind === "object" && key !== void 0 && !key.startsWith("*") && !(key in itemOfList.properties)) problems.push({
10247
- message: `<For> \`key\` "${key}" is not a property of \`${itemOfList.type}\` (properties: ${describeMembers(itemOfList)})`,
10248
- offset: offsetOf(text, `key="${key}"`, from)
10249
- });
10250
- checkNodes(text, node.children, withLoop(scope, item, itemOfList, index), problems);
10251
- }
10176
+ function describeLoopVariable(variable, shape) {
10177
+ const list = variable.list ? ` \`${variable.list}\`` : "";
10178
+ if (variable.role === "item") return `each item of${list}${typeSuffix(shape)}`;
10179
+ return `the position in${list}`;
10252
10180
  }
10253
- function checkBindings(text, value, from, scope, problems) {
10254
- for (const [binding, source = ""] of value.matchAll(BINDING)) {
10255
- const expr = parseBinding(source);
10256
- if (!expr) continue;
10257
- const bindingOffset = offsetOf(text, binding, from);
10258
- for (const { name, owner } of unknownMembers(expr, scope)) problems.push({
10259
- message: `\`${name}\` is not a property of \`${owner.type}\` (properties: ${describeMembers(owner)})`,
10260
- offset: memberOffset(bindingOffset, binding, name)
10261
- });
10262
- }
10181
+ function typeSuffix(shape) {
10182
+ return shape ? `: \`${shape.type}\`` : "";
10263
10183
  }
10264
- function unknownMembers(expr, scope) {
10265
- const found = (node) => unknownMembers(node, scope);
10266
- switch (expr.type) {
10267
- case "Literal":
10268
- case "Identifier": return [];
10269
- case "Member": {
10270
- const inner = [...found(expr.object), ...found(expr.property)];
10271
- const { property } = expr;
10272
- if (property.type !== "Literal" || typeof property.value !== "string") return inner;
10273
- const owner = shapeOf(expr.object, scope);
10274
- if (owner && owner.kind !== "value" && !(property.value in membersOf(owner))) return [...inner, {
10275
- name: property.value,
10276
- owner
10277
- }];
10278
- return inner;
10279
- }
10280
- case "Unary": return found(expr.argument);
10281
- case "Binary": return [...found(expr.left), ...found(expr.right)];
10282
- case "Conditional": return [
10283
- ...found(expr.test),
10284
- ...found(expr.consequent),
10285
- ...found(expr.alternate)
10286
- ];
10184
+ function describeChildren(component) {
10185
+ switch (component.children) {
10186
+ case "none": return "no children";
10187
+ case "text": return "text and inline components";
10188
+ case "elements": return "any components";
10287
10189
  }
10288
10190
  }
10289
- function offsetOf(text, written, from) {
10290
- const at = text.indexOf(written, from);
10291
- return at < 0 ? from : at;
10292
- }
10293
- function memberOffset(bindingOffset, binding, name) {
10294
- const escapedName = name.replaceAll("$", "\\$");
10295
- const match = new RegExp(`(?<=\\.\\s*)${escapedName}(?![\\w$])`).exec(binding);
10296
- return match ? bindingOffset + match.index : bindingOffset;
10297
- }
10298
10191
  //#endregion
10299
10192
  //#region src/lsp/markup.ts
10300
- const NAME = /[A-Za-z0-9_.:-]/;
10301
- const ATTRIBUTE_NAME_END = /[\s=/>"']/;
10193
+ const TAG_NAME = /[A-Za-z0-9_.:-]/;
10194
+ const ATTRIBUTE_NAME = /[^\s=/>"']/;
10302
10195
  const WHITESPACE = /\s/;
10303
10196
  function markupContextAt(text, offset) {
10304
- const open = [];
10197
+ const openElements = [];
10305
10198
  let index = 0;
10306
10199
  while (index < offset) if (text.startsWith("<!--", index)) {
10307
10200
  const end = text.indexOf("-->", index + 4);
@@ -10309,39 +10202,39 @@ function markupContextAt(text, offset) {
10309
10202
  index = end + 3;
10310
10203
  } else if (text.startsWith("</", index)) {
10311
10204
  const nameStart = index + 2;
10312
- const nameEnd = readWhile(text, nameStart, NAME);
10205
+ const nameEnd = readWhile(text, nameStart, TAG_NAME);
10313
10206
  const close = text.indexOf(">", nameEnd);
10314
10207
  if (offset <= nameEnd || close < 0 || close >= offset) return {
10315
10208
  kind: "closeTag",
10316
10209
  start: nameStart,
10317
- open: open.at(-1)
10210
+ open: openElements.at(-1)
10318
10211
  };
10319
- closeElement(open, text.slice(nameStart, nameEnd));
10212
+ closeElement(openElements, text.slice(nameStart, nameEnd));
10320
10213
  index = close + 1;
10321
10214
  } else if (text[index] === "<") {
10322
- const tag = readTag(text, index, offset, open);
10215
+ const tag = readTag(text, index, offset, openElements);
10323
10216
  if (tag.context) return tag.context;
10324
- if (tag.element && !tag.selfClosing) open.push(tag.element);
10217
+ if (tag.element && !tag.selfClosing) openElements.push(tag.element);
10325
10218
  index = tag.end;
10326
10219
  } else if (text.startsWith("{{", index)) {
10327
10220
  const end = text.indexOf("}}", index + 2);
10328
10221
  if (end < 0 || offset <= end) return {
10329
10222
  kind: "binding",
10330
10223
  start: index + 2,
10331
- loopVariables: loopVariables(open)
10224
+ loopVariables: loopVariables(openElements)
10332
10225
  };
10333
10226
  index = end + 2;
10334
10227
  } else index++;
10335
10228
  return { kind: "text" };
10336
10229
  }
10337
- function readTag(text, start, offset, open) {
10230
+ function readTag(text, start, offset, openElements) {
10338
10231
  const nameStart = start + 1;
10339
- const nameEnd = readWhile(text, nameStart, NAME);
10232
+ const nameEnd = readWhile(text, nameStart, TAG_NAME);
10340
10233
  if (offset <= nameEnd) return {
10341
10234
  context: {
10342
10235
  kind: "tagName",
10343
10236
  start: nameStart,
10344
- parents: [...open]
10237
+ parents: [...openElements]
10345
10238
  },
10346
10239
  end: nameEnd
10347
10240
  };
@@ -10376,7 +10269,7 @@ function readTag(text, start, offset, open) {
10376
10269
  end: index + 1
10377
10270
  };
10378
10271
  const attributeStart = index;
10379
- index = readUntil(text, index + 1, ATTRIBUTE_NAME_END);
10272
+ index = readWhile(text, index + 1, ATTRIBUTE_NAME);
10380
10273
  const name = text.slice(attributeStart, index);
10381
10274
  if (offset <= index) return {
10382
10275
  context: {
@@ -10396,7 +10289,7 @@ function readTag(text, start, offset, open) {
10396
10289
  if (quote === "\"" || quote === "'") {
10397
10290
  const close = text.indexOf(quote, valueStart + 1);
10398
10291
  if (close < 0 || offset <= close) return {
10399
- context: valueContext(text.slice(valueStart + 1, offset), valueStart + 1, element, name, open),
10292
+ context: valueContext(text.slice(valueStart + 1, offset), valueStart + 1, element, name, openElements),
10400
10293
  end: offset
10401
10294
  };
10402
10295
  attributes[name] = text.slice(valueStart + 1, close);
@@ -10409,7 +10302,7 @@ function readTag(text, start, offset, open) {
10409
10302
  start: valueStart,
10410
10303
  element,
10411
10304
  attribute: name,
10412
- loopVariables: loopVariables(open)
10305
+ loopVariables: loopVariables(openElements)
10413
10306
  },
10414
10307
  end: valueEnd
10415
10308
  };
@@ -10418,8 +10311,8 @@ function readTag(text, start, offset, open) {
10418
10311
  }
10419
10312
  }
10420
10313
  }
10421
- function valueContext(valueBeforeCursor, valueStart, element, attribute, open) {
10422
- const variables = loopVariables(open);
10314
+ function valueContext(valueBeforeCursor, valueStart, element, attribute, openElements) {
10315
+ const variables = loopVariables(openElements);
10423
10316
  const bindingStart = valueBeforeCursor.lastIndexOf("{{");
10424
10317
  if (bindingStart >= 0 && !valueBeforeCursor.includes("}}", bindingStart)) return {
10425
10318
  kind: "binding",
@@ -10434,18 +10327,18 @@ function valueContext(valueBeforeCursor, valueStart, element, attribute, open) {
10434
10327
  loopVariables: variables
10435
10328
  };
10436
10329
  }
10437
- function closeElement(open, tag) {
10438
- for (let index = open.length - 1; index >= 0; index--) if (open[index].tag === tag) {
10439
- open.length = index;
10330
+ function closeElement(openElements, tag) {
10331
+ for (let index = openElements.length - 1; index >= 0; index--) if (openElements[index].tag === tag) {
10332
+ openElements.length = index;
10440
10333
  return;
10441
10334
  }
10442
10335
  }
10443
- function loopVariables(open) {
10336
+ function loopVariables(openElements) {
10444
10337
  const variables = [];
10445
- for (const element of open) {
10338
+ for (const element of openElements) {
10446
10339
  if (element.tag !== "For") continue;
10447
10340
  const { item = "item", index = "index", each = "" } = element.attributes;
10448
- const list = each.replace(/^\s*{{\s*|\s*}}\s*$/g, "");
10341
+ const list = bindingSource(each);
10449
10342
  variables.push({
10450
10343
  name: item,
10451
10344
  role: "item",
@@ -10458,6 +10351,9 @@ function loopVariables(open) {
10458
10351
  }
10459
10352
  return variables;
10460
10353
  }
10354
+ function bindingSource(value) {
10355
+ return value.replace(/^\s*{{\s*|\s*}}\s*$/g, "");
10356
+ }
10461
10357
  function readWhile(text, index, pattern) {
10462
10358
  while (index < text.length && pattern.test(text[index])) index++;
10463
10359
  return index;
@@ -10466,7 +10362,6 @@ function readUntil(text, index, pattern) {
10466
10362
  while (index < text.length && !pattern.test(text[index])) index++;
10467
10363
  return index;
10468
10364
  }
10469
- /** The start and end of the run of `pattern` characters around `offset`. */
10470
10365
  function wordAt(text, offset, pattern) {
10471
10366
  let start = offset;
10472
10367
  while (start > 0 && pattern.test(text[start - 1])) start--;
@@ -10476,96 +10371,163 @@ function wordAt(text, offset, pattern) {
10476
10371
  };
10477
10372
  }
10478
10373
  //#endregion
10479
- //#region src/lsp/features.ts
10480
- const TAG_NAME = /[A-Za-z0-9_.:-]/;
10481
- const ATTRIBUTE_NAME = /[^\s=/>"']/;
10482
- const IMAGE_ATTRIBUTES = ["src", "image"];
10483
- const IDENTIFIER = /[\w$]/;
10374
+ //#region src/lsp/shapes.ts
10375
+ const NUMBER_SHAPE = {
10376
+ kind: "value",
10377
+ type: "number"
10378
+ };
10484
10379
  const MEMBER_CHAIN = /([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*\.\s*$/;
10485
- const VALUE = /[^\s"']/;
10380
+ function parseBinding(source) {
10381
+ try {
10382
+ return parseExpression(source);
10383
+ } catch (error) {
10384
+ if (error instanceof ScriptParseError) return;
10385
+ throw error;
10386
+ }
10387
+ }
10388
+ function shapeOf(expression, scope) {
10389
+ switch (expression.type) {
10390
+ case "Identifier":
10391
+ if (scope.loops.has(expression.name)) return scope.loops.get(expression.name);
10392
+ return scope.state[expression.name];
10393
+ case "Member": {
10394
+ const owner = shapeOf(expression.object, scope);
10395
+ if (!owner) return;
10396
+ const { property } = expression;
10397
+ if (property.type === "Literal" && typeof property.value === "string") return membersOf(owner)[property.value];
10398
+ if (owner.kind === "list") return owner.item;
10399
+ return;
10400
+ }
10401
+ default: return;
10402
+ }
10403
+ }
10404
+ function membersOf(shape) {
10405
+ switch (shape.kind) {
10406
+ case "object": return shape.properties;
10407
+ case "list": return { length: NUMBER_SHAPE };
10408
+ case "value": return {};
10409
+ }
10410
+ }
10411
+ function describeMembers(shape) {
10412
+ const names = Object.keys(membersOf(shape));
10413
+ if (names.length === 0) return "none";
10414
+ return names.join(", ");
10415
+ }
10416
+ function loopScope(variables, state) {
10417
+ const loops = /* @__PURE__ */ new Map();
10418
+ for (const variable of variables) if (variable.role === "index") loops.set(variable.name, NUMBER_SHAPE);
10419
+ else loops.set(variable.name, itemShape(variable.list, {
10420
+ state,
10421
+ loops
10422
+ }));
10423
+ return {
10424
+ state,
10425
+ loops
10426
+ };
10427
+ }
10428
+ function bindingScope(loopVariables, page) {
10429
+ const shapes = page.state?.shapes;
10430
+ if (!shapes) return;
10431
+ return loopScope(loopVariables, shapes);
10432
+ }
10433
+ function itemShape(each, scope) {
10434
+ const list = parseBinding(bindingSource(each));
10435
+ if (!list) return;
10436
+ const shape = shapeOf(list, scope);
10437
+ if (shape?.kind === "list") return shape.item;
10438
+ }
10439
+ function withLoop(scope, item, itemOfList, index) {
10440
+ const loops = new Map(scope.loops);
10441
+ loops.set(item, itemOfList);
10442
+ loops.set(index, NUMBER_SHAPE);
10443
+ return {
10444
+ state: scope.state,
10445
+ loops
10446
+ };
10447
+ }
10448
+ function isMember(text, bindingStart, wordStart) {
10449
+ return text.slice(bindingStart, wordStart).trimEnd().endsWith(".");
10450
+ }
10451
+ function ownerOf(text, bindingStart, wordStart, scope) {
10452
+ const chain = MEMBER_CHAIN.exec(text.slice(bindingStart, wordStart))?.[1];
10453
+ if (chain === void 0) return;
10454
+ const expression = parseBinding(chain);
10455
+ if (!expression || !scope) return;
10456
+ return shapeOf(expression, scope);
10457
+ }
10458
+ //#endregion
10459
+ //#region src/lsp/completion.ts
10460
+ const IMAGE_ATTRIBUTES = ["src", "image"];
10461
+ const IDENTIFIER_BEFORE_CURSOR = /[A-Za-z_$][\w$]*$/;
10486
10462
  const CONTROL_FLOW_SNIPPETS = {
10487
10463
  If: "If condition=\"{{ $1 }}\">$0</If>",
10488
10464
  ElseIf: "ElseIf condition=\"{{ $1 }}\">$0</ElseIf>",
10489
10465
  Else: "Else>$0</Else>",
10490
10466
  For: "For each=\"{{ $1 }}\" key=\"$2\">$0</For>"
10491
10467
  };
10492
- const CONTROL_FLOW_DOCS = {
10493
- If: "Shows its children when `condition` is true.",
10494
- ElseIf: "Follows an `<If>` or `<ElseIf>`: shows its children when the ones before are false and `condition` is true.",
10495
- Else: "Follows an `<If>` or `<ElseIf>`: shows its children when the ones before are false.",
10496
- For: "Repeats its children for each item of `each`, as `item` and `index`. `key` is a property of each item, `*this` for the item itself, or `*index` to match items by position."
10497
- };
10498
- const CONTROL_FLOW_ATTRIBUTE_DOCS = {
10499
- condition: "A single `{{ }}` binding",
10500
- each: "The list to repeat, as a single `{{ }}` binding",
10501
- item: "The name of each item (default `item`)",
10502
- index: "The name of each position (default `index`)",
10503
- key: "A property of each item, `*this` or `*index`"
10504
- };
10505
- function complete(document, offset, page, { components }) {
10468
+ function complete(document, offset, page, components) {
10506
10469
  const text = document.getText();
10507
10470
  const context = markupContextAt(text, offset);
10508
- const range = (start) => ({
10509
- start: document.positionAt(start),
10510
- end: document.positionAt(offset)
10511
- });
10512
10471
  switch (context.kind) {
10513
- case "tagName": return tagCompletions(context.parents, components).map((item) => ({
10514
- ...item,
10515
- textEdit: {
10516
- range: range(context.start),
10517
- newText: item.insertText ?? item.label
10518
- }
10519
- }));
10472
+ case "tagName": {
10473
+ const items = tagCompletions(context.parents, components);
10474
+ return replacingFrom(document, context.start, offset, items);
10475
+ }
10520
10476
  case "closeTag":
10521
10477
  if (!context.open) return [];
10522
10478
  return [{
10523
10479
  label: `/${context.open.tag}`,
10524
10480
  kind: import_main.CompletionItemKind.Property,
10525
10481
  textEdit: {
10526
- range: range(context.start),
10482
+ range: rangeFrom(document, context.start, offset),
10527
10483
  newText: `${context.open.tag}>`
10528
10484
  }
10529
10485
  }];
10530
- case "attributeName": return attributeCompletions(context.element, components).map((item) => ({
10531
- ...item,
10532
- textEdit: {
10533
- range: range(context.start),
10534
- newText: item.insertText ?? item.label
10535
- }
10536
- }));
10486
+ case "attributeName": {
10487
+ const items = attributeCompletions(context.element, components);
10488
+ return replacingFrom(document, context.start, offset, items);
10489
+ }
10537
10490
  case "attributeValue": {
10538
- const scope = shapeScope(context.loopVariables, page);
10539
- return valueCompletions(context.element, context.attribute, page, scope, components).map((item) => ({
10540
- ...item,
10541
- textEdit: {
10542
- range: range(context.start),
10543
- newText: item.label
10544
- }
10545
- }));
10491
+ const scope = bindingScope(context.loopVariables, page);
10492
+ const items = valueCompletions(context.element, context.attribute, page, scope, components);
10493
+ return replacingFrom(document, context.start, offset, items);
10546
10494
  }
10547
10495
  case "binding": {
10548
10496
  const typed = text.slice(context.start, offset);
10549
- const wordStart = offset - (/[A-Za-z_$][\w$]*$/.exec(typed)?.[0] ?? "").length;
10550
- const scope = shapeScope(context.loopVariables, page);
10551
- return (isMember(text, context.start, wordStart) ? memberCompletions(ownerOf(text, context.start, wordStart, scope)) : bindingCompletions(context.loopVariables, page, scope)).map((item) => ({
10552
- ...item,
10553
- textEdit: {
10554
- range: range(wordStart),
10555
- newText: item.label
10556
- }
10557
- }));
10497
+ const wordStart = offset - (IDENTIFIER_BEFORE_CURSOR.exec(typed)?.[0] ?? "").length;
10498
+ const scope = bindingScope(context.loopVariables, page);
10499
+ let items;
10500
+ if (isMember(text, context.start, wordStart)) items = memberCompletions(ownerOf(text, context.start, wordStart, scope));
10501
+ else items = bindingCompletions(context.loopVariables, page, scope);
10502
+ return replacingFrom(document, wordStart, offset, items);
10558
10503
  }
10559
10504
  default: return [];
10560
10505
  }
10561
10506
  }
10507
+ function rangeFrom(document, start, offset) {
10508
+ return {
10509
+ start: document.positionAt(start),
10510
+ end: document.positionAt(offset)
10511
+ };
10512
+ }
10513
+ function replacingFrom(document, start, offset, items) {
10514
+ return items.map((item) => ({
10515
+ ...item,
10516
+ textEdit: {
10517
+ range: rangeFrom(document, start, offset),
10518
+ newText: item.insertText ?? item.label
10519
+ }
10520
+ }));
10521
+ }
10562
10522
  function tagCompletions(parents, components) {
10563
10523
  if (parents.length === 0) {
10564
10524
  const root = components[ROOT_TAG];
10565
- return root ? [componentItem(root, "0")] : [];
10525
+ if (!root) return [];
10526
+ return [componentItem(root, "0")];
10566
10527
  }
10567
10528
  const parent = [...parents].reverse().find((element) => !(element.tag in CONTROL_FLOW_TAGS));
10568
- const parentManifest = parent && components[parent.tag];
10529
+ let parentManifest;
10530
+ if (parent) parentManifest = components[parent.tag];
10569
10531
  const items = Object.values(components).filter((component) => !parentManifest || canHaveChildren(parentManifest, component)).map((component) => componentItem(component, "1"));
10570
10532
  for (const [tag, snippet] of Object.entries(CONTROL_FLOW_SNIPPETS)) items.push({
10571
10533
  label: tag,
@@ -10582,7 +10544,8 @@ function tagCompletions(parents, components) {
10582
10544
  return items;
10583
10545
  }
10584
10546
  function componentItem(component, sortGroup) {
10585
- const snippet = component.children === "none" ? `${component.name}$1 />` : `${component.name}$1>$0</${component.name}>`;
10547
+ let snippet = `${component.name}$1>$0</${component.name}>`;
10548
+ if (component.children === "none") snippet = `${component.name}$1 />`;
10586
10549
  return {
10587
10550
  label: component.name,
10588
10551
  kind: import_main.CompletionItemKind.Class,
@@ -10597,18 +10560,18 @@ function componentItem(component, sortGroup) {
10597
10560
  };
10598
10561
  }
10599
10562
  function attributeCompletions(element, components) {
10600
- const written = (name) => name in element.attributes;
10563
+ const isWritten = (name) => name in element.attributes;
10601
10564
  const controlFlow = CONTROL_FLOW_TAGS[element.tag];
10602
- if (controlFlow) return controlFlow.attributes.filter((name) => !written(name)).map((name) => ({
10565
+ if (controlFlow) return controlFlow.attributes.filter((name) => !isWritten(name)).map((name) => ({
10603
10566
  label: name,
10604
10567
  kind: import_main.CompletionItemKind.Property,
10605
10568
  detail: CONTROL_FLOW_ATTRIBUTE_DOCS[name] ?? name,
10606
- insertText: name === "condition" || name === "each" ? `${name}="{{ $1 }}"` : `${name}="$1"`,
10569
+ insertText: controlFlowAttributeSnippet(name),
10607
10570
  insertTextFormat: import_main.InsertTextFormat.Snippet
10608
10571
  }));
10609
10572
  const component = components[element.tag];
10610
10573
  if (!component) return [];
10611
- const props = Object.entries(component.props).filter(([name]) => !written(name)).map(([name, type]) => ({
10574
+ const props = Object.entries(component.props).filter(([name]) => !isWritten(name)).map(([name, type]) => ({
10612
10575
  label: name,
10613
10576
  kind: import_main.CompletionItemKind.Property,
10614
10577
  detail: describeType(type),
@@ -10616,7 +10579,7 @@ function attributeCompletions(element, components) {
10616
10579
  insertTextFormat: import_main.InsertTextFormat.Snippet,
10617
10580
  sortText: `0${name}`
10618
10581
  }));
10619
- const events = component.events.map((event) => eventProp(event)).filter((name) => !written(name)).map((name) => ({
10582
+ const events = component.events.map((event) => eventProp(event)).filter((name) => !isWritten(name)).map((name) => ({
10620
10583
  label: name,
10621
10584
  kind: import_main.CompletionItemKind.Event,
10622
10585
  detail: "a mutation or action in controller.ts",
@@ -10626,27 +10589,20 @@ function attributeCompletions(element, components) {
10626
10589
  }));
10627
10590
  return [...props, ...events];
10628
10591
  }
10592
+ function controlFlowAttributeSnippet(name) {
10593
+ if (name === "condition" || name === "each") return `${name}="{{ $1 }}"`;
10594
+ return `${name}="$1"`;
10595
+ }
10629
10596
  function valueCompletions(element, attribute, page, scope, components) {
10630
- if (element.tag === "For" && attribute === "key") {
10631
- const item = scope && itemShape(element.attributes.each ?? "", scope);
10632
- return [
10633
- ...item?.kind === "object" ? memberCompletions(item) : [],
10634
- {
10635
- label: "*this",
10636
- kind: import_main.CompletionItemKind.Value,
10637
- detail: "the item itself"
10638
- },
10639
- {
10640
- label: "*index",
10641
- kind: import_main.CompletionItemKind.Value,
10642
- detail: "match items by position"
10643
- }
10644
- ];
10645
- }
10597
+ if (element.tag === "For" && attribute === "key") return forKeyCompletions(element, scope);
10646
10598
  const component = components[element.tag];
10647
10599
  if (!component) return [];
10648
- if (isEventProp(attribute) && component.events.includes(eventName(attribute))) return handlerItems(page);
10600
+ if (isEventProp(attribute) && component.events.includes(eventName(attribute))) return handlerCompletions(page);
10649
10601
  const type = component.props[attribute];
10602
+ if (type && isWordsOr(type)) return type.words.map((value) => ({
10603
+ label: value,
10604
+ kind: import_main.CompletionItemKind.EnumMember
10605
+ }));
10650
10606
  if (Array.isArray(type)) return type.map((value) => ({
10651
10607
  label: value,
10652
10608
  kind: import_main.CompletionItemKind.EnumMember
@@ -10661,27 +10617,52 @@ function valueCompletions(element, attribute, page, scope, components) {
10661
10617
  }));
10662
10618
  return [];
10663
10619
  }
10664
- function handlerItems(page) {
10620
+ function forKeyCompletions(element, scope) {
10621
+ let properties = [];
10622
+ if (scope) {
10623
+ const item = itemShape(element.attributes.each ?? "", scope);
10624
+ if (item?.kind === "object") properties = memberCompletions(item);
10625
+ }
10626
+ return [
10627
+ ...properties,
10628
+ {
10629
+ label: "*this",
10630
+ kind: import_main.CompletionItemKind.Value,
10631
+ detail: "the item itself"
10632
+ },
10633
+ {
10634
+ label: "*index",
10635
+ kind: import_main.CompletionItemKind.Value,
10636
+ detail: "match items by position"
10637
+ }
10638
+ ];
10639
+ }
10640
+ function handlerCompletions(page) {
10665
10641
  const { controller } = page;
10666
10642
  if (!controller) return [];
10667
- return [...(controller.mutations ?? []).map((name) => ({
10643
+ const mutations = (controller.mutations ?? []).map((name) => ({
10668
10644
  label: name,
10669
10645
  kind: import_main.CompletionItemKind.Method,
10670
10646
  detail: "mutation"
10671
- })), ...(controller.actions ?? []).map((name) => ({
10647
+ }));
10648
+ const actions = (controller.actions ?? []).map((name) => ({
10672
10649
  label: name,
10673
10650
  kind: import_main.CompletionItemKind.Function,
10674
10651
  detail: "action"
10675
- }))];
10652
+ }));
10653
+ return [...mutations, ...actions];
10676
10654
  }
10677
10655
  function bindingCompletions(loopVariables, page, scope) {
10678
10656
  const items = /* @__PURE__ */ new Map();
10679
- for (const key of page.state?.keys ?? []) items.set(key, {
10680
- label: key,
10681
- kind: import_main.CompletionItemKind.Field,
10682
- detail: scope?.state[key] ? `state: ${scope.state[key].type}` : "state",
10683
- sortText: `1${key}`
10684
- });
10657
+ for (const key of page.state?.keys ?? []) {
10658
+ const shape = scope?.state[key];
10659
+ items.set(key, {
10660
+ label: key,
10661
+ kind: import_main.CompletionItemKind.Field,
10662
+ detail: shape ? `state: ${shape.type}` : "state",
10663
+ sortText: `1${key}`
10664
+ });
10665
+ }
10685
10666
  for (const variable of loopVariables) items.set(variable.name, {
10686
10667
  label: variable.name,
10687
10668
  kind: import_main.CompletionItemKind.Variable,
@@ -10698,232 +10679,109 @@ function memberCompletions(owner) {
10698
10679
  detail: shape.type
10699
10680
  }));
10700
10681
  }
10701
- function shapeScope(loopVariables, page) {
10702
- const shapes = page.state?.shapes;
10703
- return shapes && loopScope(loopVariables, shapes);
10704
- }
10705
- function isMember(text, bindingStart, start) {
10706
- return text.slice(bindingStart, start).trimEnd().endsWith(".");
10707
- }
10708
- function ownerOf(text, bindingStart, start, scope) {
10709
- const chain = MEMBER_CHAIN.exec(text.slice(bindingStart, start))?.[1];
10710
- const expr = chain === void 0 ? void 0 : parseBinding(chain);
10711
- return expr && scope && shapeOf(expr, scope);
10712
- }
10713
- function hover(document, offset, page, { components }) {
10714
- const found = symbolAt(document.getText(), offset, page);
10715
- if (!found) return null;
10716
- const markdown = describeSymbol(found.symbol, page, components);
10717
- if (!markdown) return null;
10718
- return {
10719
- contents: {
10720
- kind: import_main.MarkupKind.Markdown,
10721
- value: markdown
10722
- },
10723
- range: {
10724
- start: document.positionAt(found.start),
10725
- end: document.positionAt(found.end)
10726
- }
10727
- };
10728
- }
10729
- function definition(document, offset, page) {
10730
- const found = symbolAt(document.getText(), offset, page);
10731
- if (!found) return null;
10732
- const { symbol } = found;
10733
- if (symbol.kind === "handler" && page.controller) return locationIn(page.controller.file, page.controller.text, page.controller.offsets[symbol.name]);
10734
- if (symbol.kind === "state" && page.state) return locationIn(page.state.file, page.state.text, page.state.offsets[symbol.name]);
10735
- if (symbol.kind === "property" && symbol.shape.declaration) {
10736
- const { file, line, character } = symbol.shape.declaration;
10737
- const position = {
10738
- line,
10739
- character
10740
- };
10741
- return {
10742
- uri: pathToFileURL(file).href,
10743
- range: {
10744
- start: position,
10745
- end: position
10746
- }
10747
- };
10682
+ //#endregion
10683
+ //#region src/lsp/check-members.ts
10684
+ const BINDING = /{{([\s\S]*?)}}/g;
10685
+ function checkMembers(text, state) {
10686
+ let nodes;
10687
+ try {
10688
+ nodes = parseDmlToIR(text, {
10689
+ locations: true,
10690
+ whitespace: "preserve"
10691
+ });
10692
+ } catch {
10693
+ return [];
10748
10694
  }
10749
- if (symbol.kind === "static" && page.root) return locationIn(join(page.root, symbol.path), "", 0);
10750
- return null;
10695
+ const problems = [];
10696
+ checkNodes(text, nodes, {
10697
+ state,
10698
+ loops: /* @__PURE__ */ new Map()
10699
+ }, problems);
10700
+ return problems;
10751
10701
  }
10752
- function symbolAt(text, offset, page) {
10753
- const context = markupContextAt(text, offset);
10754
- switch (context.kind) {
10755
- case "tagName":
10756
- case "closeTag": {
10757
- const { start, end } = wordAt(text, offset, TAG_NAME);
10758
- return {
10759
- symbol: {
10760
- kind: "component",
10761
- tag: text.slice(start, end)
10762
- },
10763
- start,
10764
- end
10765
- };
10766
- }
10767
- case "attributeName": {
10768
- const { start, end } = wordAt(text, offset, ATTRIBUTE_NAME);
10769
- const name = text.slice(start, end);
10770
- return name ? {
10771
- symbol: {
10772
- kind: "attribute",
10773
- tag: context.element.tag,
10774
- name
10775
- },
10776
- start,
10777
- end
10778
- } : null;
10779
- }
10780
- case "attributeValue": {
10781
- const { start, end } = wordAt(text, offset, VALUE);
10782
- const value = text.slice(start, end);
10783
- if (isEventProp(context.attribute)) return {
10784
- symbol: {
10785
- kind: "handler",
10786
- name: value
10787
- },
10788
- start,
10789
- end
10790
- };
10791
- if (page.statics?.includes(value)) return {
10792
- symbol: {
10793
- kind: "static",
10794
- path: value
10795
- },
10796
- start,
10797
- end
10798
- };
10799
- return null;
10702
+ function checkNodes(text, nodes, scope, problems) {
10703
+ for (const node of nodes) {
10704
+ const from = node.loc?.offset ?? 0;
10705
+ if (node.type === "Text") {
10706
+ checkBindings(text, node.content, from, scope, problems);
10707
+ continue;
10800
10708
  }
10801
- case "binding": {
10802
- const { start, end } = wordAt(text, offset, IDENTIFIER);
10803
- const name = text.slice(start, end);
10804
- if (!name) return null;
10805
- const scope = shapeScope(context.loopVariables, page);
10806
- if (isMember(text, context.start, start)) {
10807
- const owner = ownerOf(text, context.start, start, scope);
10808
- const shape = owner && membersOf(owner)[name];
10809
- return owner && shape ? {
10810
- symbol: {
10811
- kind: "property",
10812
- name,
10813
- owner,
10814
- shape
10815
- },
10816
- start,
10817
- end
10818
- } : null;
10819
- }
10820
- const variable = [...context.loopVariables].reverse().find((candidate) => candidate.name === name);
10821
- if (variable) {
10822
- const shape = scope?.loops.get(name);
10823
- return {
10824
- symbol: shape ? {
10825
- kind: "loopVariable",
10826
- variable,
10827
- shape
10828
- } : {
10829
- kind: "loopVariable",
10830
- variable
10831
- },
10832
- start,
10833
- end
10834
- };
10835
- }
10836
- return {
10837
- symbol: {
10838
- kind: "state",
10839
- name
10840
- },
10841
- start,
10842
- end
10843
- };
10709
+ for (const value of Object.values(node.attributes)) checkBindings(text, value, from, scope, problems);
10710
+ if (node.tag !== "For") {
10711
+ checkNodes(text, node.children, scope, problems);
10712
+ continue;
10844
10713
  }
10845
- default: return null;
10714
+ const { each = "", item = "item", index = "index", key } = node.attributes;
10715
+ const itemOfList = itemShape(each, scope);
10716
+ if (key !== void 0 && !key.startsWith("*") && itemOfList?.kind === "object" && !(key in itemOfList.properties)) problems.push({
10717
+ message: `The <For> \`key\` "${key}" is not a property of \`${itemOfList.type}\`. It has: ${describeMembers(itemOfList)}`,
10718
+ offset: offsetOf(text, `key="${key}"`, from)
10719
+ });
10720
+ checkNodes(text, node.children, withLoop(scope, item, itemOfList, index), problems);
10846
10721
  }
10847
10722
  }
10848
- function describeSymbol(symbol, page, components) {
10849
- switch (symbol.kind) {
10850
- case "component": {
10851
- const controlFlow = CONTROL_FLOW_DOCS[symbol.tag];
10852
- if (controlFlow) return `**<${symbol.tag}>** · control flow\n\n${controlFlow}`;
10853
- const component = components[symbol.tag];
10854
- return component ? describeComponent(component) : null;
10855
- }
10856
- case "attribute": {
10857
- if (CONTROL_FLOW_TAGS[symbol.tag]) {
10858
- const doc = CONTROL_FLOW_ATTRIBUTE_DOCS[symbol.name];
10859
- return doc ? `\`${symbol.name}\`: ${doc}` : null;
10860
- }
10861
- const component = components[symbol.tag];
10862
- if (!component) return null;
10863
- const type = component.props[symbol.name];
10864
- if (type) return `\`${symbol.name}\`: ${describeType(type)}`;
10865
- if (symbol.name.startsWith(DATA_PREFIX)) return `\`${symbol.name}\`: reaches this element's handlers as \`event.data.${dataKey(symbol.name)}\``;
10866
- if (isEventProp(symbol.name) && component.events.includes(eventName(symbol.name))) return `\`${symbol.name}\`: runs a mutation or action in controller.ts on \`${eventName(symbol.name)}\``;
10867
- return null;
10868
- }
10869
- case "handler": {
10870
- const { controller } = page;
10871
- if (controller?.mutations?.includes(symbol.name)) return `mutation \`${symbol.name}\` in controller.ts`;
10872
- if (controller?.actions?.includes(symbol.name)) return `action \`${symbol.name}\` in controller.ts`;
10873
- return null;
10874
- }
10875
- case "state": return page.state?.keys?.includes(symbol.name) ? `state \`${symbol.name}\` in state.ts${typeSuffix(page.state.shapes?.[symbol.name])}` : null;
10876
- case "loopVariable": return `\`${symbol.variable.name}\`: ${describeLoopVariable(symbol.variable, symbol.shape)}`;
10877
- case "property": return `\`${symbol.name}\`: \`${symbol.shape.type}\`, a property of \`${symbol.owner.type}\``;
10878
- case "static": return `image \`${symbol.path}\``;
10723
+ function checkBindings(text, value, from, scope, problems) {
10724
+ for (const [binding, source = ""] of value.matchAll(BINDING)) {
10725
+ const expression = parseBinding(source);
10726
+ if (!expression) continue;
10727
+ const bindingOffset = offsetOf(text, binding, from);
10728
+ for (const { name, owner } of unknownMembers(expression, scope)) problems.push({
10729
+ message: `\`${name}\` is not a property of \`${owner.type}\`. It has: ${describeMembers(owner)}`,
10730
+ offset: memberOffset(bindingOffset, binding, name)
10731
+ });
10879
10732
  }
10880
10733
  }
10881
- function describeComponent(component) {
10882
- const lines = [`**<${component.name}>** · ${describeChildren(component)}`];
10883
- const props = Object.entries(component.props);
10884
- if (props.length > 0) lines.push("", ...props.map(([name, type]) => `- \`${name}\`: ${describeType(type)}`));
10885
- if (component.events.length > 0) lines.push("", `Events: ${component.events.map((event) => `\`${eventProp(event)}\``).join(", ")}`);
10886
- return lines.join("\n");
10887
- }
10888
- function describeChildren(component) {
10889
- switch (component.children) {
10890
- case "none": return "no children";
10891
- case "text": return "text and inline components";
10892
- case "elements": return "any components";
10734
+ function unknownMembers(expression, scope) {
10735
+ const unknownIn = (node) => unknownMembers(node, scope);
10736
+ switch (expression.type) {
10737
+ case "Literal":
10738
+ case "Identifier": return [];
10739
+ case "Member": {
10740
+ const inner = [...unknownIn(expression.object), ...unknownIn(expression.property)];
10741
+ const { property } = expression;
10742
+ if (property.type !== "Literal" || typeof property.value !== "string") return inner;
10743
+ const owner = shapeOf(expression.object, scope);
10744
+ if (owner && owner.kind !== "value" && !(property.value in membersOf(owner))) return [...inner, {
10745
+ name: property.value,
10746
+ owner
10747
+ }];
10748
+ return inner;
10749
+ }
10750
+ case "Unary": return unknownIn(expression.argument);
10751
+ case "Binary": return [...unknownIn(expression.left), ...unknownIn(expression.right)];
10752
+ case "Conditional": return [
10753
+ ...unknownIn(expression.test),
10754
+ ...unknownIn(expression.consequent),
10755
+ ...unknownIn(expression.alternate)
10756
+ ];
10893
10757
  }
10894
10758
  }
10895
- function describeType(type) {
10896
- if (typeof type === "string") return type;
10897
- return type.map((value) => `"${value}"`).join(" | ");
10898
- }
10899
- function describeLoopVariable(variable, shape) {
10900
- const list = variable.list ? ` \`${variable.list}\`` : "";
10901
- return variable.role === "item" ? `each item of${list}${typeSuffix(shape)}` : `the position in${list}`;
10902
- }
10903
- function typeSuffix(shape) {
10904
- return shape ? `: \`${shape.type}\`` : "";
10759
+ function offsetOf(text, written, from) {
10760
+ const foundAt = text.indexOf(written, from);
10761
+ if (foundAt < 0) return from;
10762
+ return foundAt;
10905
10763
  }
10906
- function locationIn(file, text, offset) {
10907
- if (offset === void 0) return null;
10908
- const position = TextDocument.create("", "typescript", 0, text).positionAt(offset);
10909
- return {
10910
- uri: pathToFileURL(file).href,
10911
- range: {
10912
- start: position,
10913
- end: position
10914
- }
10915
- };
10764
+ function memberOffset(bindingOffset, binding, name) {
10765
+ const escapedName = name.replaceAll("$", "\\$");
10766
+ const match = new RegExp(`(?<=\\.\\s*)${escapedName}(?![\\w$])`).exec(binding);
10767
+ if (!match) return bindingOffset;
10768
+ return bindingOffset + match.index;
10916
10769
  }
10917
- function diagnose(document, file, page, { components }) {
10770
+ //#endregion
10771
+ //#region src/lsp/diagnostics.ts
10772
+ function diagnose(document, file, page, components) {
10918
10773
  const diagnostics = [];
10919
10774
  const context = {
10920
10775
  file,
10921
10776
  components,
10922
- onWarning: (warning) => diagnostics.push(sourceDiagnostic(document, warning, import_main.DiagnosticSeverity.Warning))
10777
+ onWarning: (warning) => {
10778
+ diagnostics.push(sourceDiagnostic(document, warning, import_main.DiagnosticSeverity.Warning));
10779
+ }
10923
10780
  };
10924
10781
  if (page.statics) context.statics = page.statics;
10925
10782
  if (page.state?.keys) context.stateKeys = page.state.keys;
10926
- const { mutations, actions } = page.controller ?? {};
10783
+ const mutations = page.controller?.mutations;
10784
+ const actions = page.controller?.actions;
10927
10785
  if (mutations && actions) context.handlers = [...mutations, ...actions];
10928
10786
  try {
10929
10787
  compileView(document.getText(), context);
@@ -10939,15 +10797,15 @@ function sourceDiagnostic(document, problem, severity) {
10939
10797
  }
10940
10798
  function diagnosticAt(document, start, message, severity) {
10941
10799
  const text = document.getText();
10942
- let end = start;
10943
- while (end < text.length && !/[\s>]/.test(text[end])) end++;
10800
+ const end = readUntil(text, start, /[\s>]/);
10801
+ const atLeastOneCharacter = Math.min(start + 1, text.length);
10944
10802
  return {
10945
10803
  severity,
10946
10804
  source: "dot",
10947
10805
  message,
10948
10806
  range: {
10949
10807
  start: document.positionAt(start),
10950
- end: document.positionAt(Math.max(end, Math.min(start + 1, text.length)))
10808
+ end: document.positionAt(Math.max(end, atLeastOneCharacter))
10951
10809
  }
10952
10810
  };
10953
10811
  }
@@ -10955,7 +10813,7 @@ function diagnosticAt(document, start, message, severity) {
10955
10813
  //#region src/lsp/page.ts
10956
10814
  async function readPage(viewFile, readText, stateTypes) {
10957
10815
  const dir = dirname(viewFile);
10958
- const root = findRoot(dir);
10816
+ const root = findDotRoot(dir);
10959
10817
  const [state, controller, statics] = await Promise.all([
10960
10818
  readState(join(dir, "state.ts"), root, readText, stateTypes),
10961
10819
  readController(join(dir, "controller.ts"), readText),
@@ -10971,20 +10829,18 @@ async function readPage(viewFile, readText, stateTypes) {
10971
10829
  async function readState(file, root, readText, stateTypes) {
10972
10830
  const text = await readText(file);
10973
10831
  if (text === void 0) return;
10974
- let keys;
10975
- let offsets;
10976
10832
  try {
10977
- ({keys, keyOffsets: offsets} = parseStateModule(text, file));
10833
+ const { keys, keyOffsets } = parseStateModule(text, file);
10834
+ return {
10835
+ file,
10836
+ text,
10837
+ keys,
10838
+ offsets: keyOffsets,
10839
+ shapes: readShapes(stateTypes, root, file, text)
10840
+ };
10978
10841
  } catch {
10979
10842
  return;
10980
10843
  }
10981
- return {
10982
- file,
10983
- text,
10984
- keys,
10985
- offsets,
10986
- shapes: readShapes(stateTypes, root, file, text)
10987
- };
10988
10844
  }
10989
10845
  function readShapes(stateTypes, root, file, text) {
10990
10846
  try {
@@ -11009,14 +10865,6 @@ async function readController(file, readText) {
11009
10865
  return;
11010
10866
  }
11011
10867
  }
11012
- function findRoot(dir) {
11013
- while (true) {
11014
- if (existsSync(join(dir, "dot.json"))) return dir;
11015
- const parent = dirname(dir);
11016
- if (parent === dir) return;
11017
- dir = parent;
11018
- }
11019
- }
11020
10868
  //#endregion
11021
10869
  //#region src/lsp/state-types.ts
11022
10870
  const MAX_DEPTH = 8;
@@ -11028,23 +10876,19 @@ const FALLBACK_OPTIONS = {
11028
10876
  noEmit: true,
11029
10877
  skipLibCheck: true
11030
10878
  };
11031
- /**
11032
- * Reads the shapes of state.ts files. Each dot keeps one TypeScript language service, so reading
11033
- * again after an edit rechecks only what changed.
11034
- */
11035
10879
  var StateTypes = class {
11036
10880
  #projects = /* @__PURE__ */ new Map();
11037
10881
  read(root, file, text) {
11038
10882
  const key = root ?? file;
11039
10883
  let project = this.#projects.get(key);
11040
10884
  if (!project) {
11041
- project = new Project(root);
10885
+ project = new TypeScriptProject(root);
11042
10886
  this.#projects.set(key, project);
11043
10887
  }
11044
10888
  return project.read(file, text);
11045
10889
  }
11046
10890
  };
11047
- var Project = class {
10891
+ var TypeScriptProject = class {
11048
10892
  #options;
11049
10893
  #fileNames;
11050
10894
  #open = /* @__PURE__ */ new Map();
@@ -11063,7 +10907,8 @@ var Project = class {
11063
10907
  },
11064
10908
  getScriptSnapshot: (file) => {
11065
10909
  const text = this.#open.get(file)?.text ?? ts.sys.readFile(file);
11066
- return text === void 0 ? void 0 : ts.ScriptSnapshot.fromString(text);
10910
+ if (text === void 0) return;
10911
+ return ts.ScriptSnapshot.fromString(text);
11067
10912
  },
11068
10913
  getCurrentDirectory: () => root ?? process.cwd(),
11069
10914
  getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
@@ -11087,7 +10932,8 @@ var Project = class {
11087
10932
  if (!program || !source) return;
11088
10933
  const checker = program.getTypeChecker();
11089
10934
  const module = checker.getSymbolAtLocation(source);
11090
- let exported = module && checker.tryGetMemberInModuleExports("default", module);
10935
+ if (!module) return;
10936
+ let exported = checker.tryGetMemberInModuleExports("default", module);
11091
10937
  if (!exported) return;
11092
10938
  if (exported.flags & ts.SymbolFlags.Alias) exported = checker.getAliasedSymbol(exported);
11093
10939
  const state = checker.getTypeOfSymbol(exported);
@@ -11099,9 +10945,9 @@ var Project = class {
11099
10945
  function readConfig(root) {
11100
10946
  const file = join(root, "tsconfig.json");
11101
10947
  if (!existsSync(file)) return;
11102
- const read = ts.readConfigFile(file, (path) => ts.sys.readFile(path));
11103
- if (read.error) return;
11104
- return ts.parseJsonConfigFileContent(read.config, ts.sys, root, void 0, file);
10948
+ const configFile = ts.readConfigFile(file, (path) => ts.sys.readFile(path));
10949
+ if (configFile.error) return;
10950
+ return ts.parseJsonConfigFileContent(configFile.config, ts.sys, root, void 0, file);
11105
10951
  }
11106
10952
  function propertyShape(checker, property, depth, seen) {
11107
10953
  const shape = typeShape(checker, checker.getTypeOfSymbol(property), depth, seen);
@@ -11128,13 +10974,15 @@ function typeShape(checker, type, depth, seen) {
11128
10974
  const inner = new Set(seen).add(present);
11129
10975
  if (checker.isArrayType(present) || checker.isTupleType(present)) {
11130
10976
  const item = checker.getIndexTypeOfType(present, ts.IndexKind.Number);
10977
+ let itemShape = {
10978
+ kind: "value",
10979
+ type: "unknown"
10980
+ };
10981
+ if (item) itemShape = typeShape(checker, item, depth + 1, inner);
11131
10982
  return {
11132
10983
  kind: "list",
11133
10984
  type: name,
11134
- item: item ? typeShape(checker, item, depth + 1, inner) : {
11135
- kind: "value",
11136
- type: "unknown"
11137
- }
10985
+ item: itemShape
11138
10986
  };
11139
10987
  }
11140
10988
  if (!isRecord(checker, present)) return {
@@ -11153,45 +11001,231 @@ function isRecord(checker, type) {
11153
11001
  return (type.isUnion() ? type.types : [type]).every((member) => (member.flags & ts.TypeFlags.Object) !== 0 && member.getCallSignatures().length === 0 && checker.getIndexInfosOfType(member).length === 0);
11154
11002
  }
11155
11003
  //#endregion
11004
+ //#region src/lsp/symbols.ts
11005
+ const IDENTIFIER = /[\w$]/;
11006
+ const VALUE = /[^\s"']/;
11007
+ function hover(document, offset, page, components) {
11008
+ const found = symbolAt(document.getText(), offset, page);
11009
+ if (!found) return null;
11010
+ const markdown = describeSymbol(found.symbol, page, components);
11011
+ if (!markdown) return null;
11012
+ return {
11013
+ contents: {
11014
+ kind: import_main.MarkupKind.Markdown,
11015
+ value: markdown
11016
+ },
11017
+ range: {
11018
+ start: document.positionAt(found.start),
11019
+ end: document.positionAt(found.end)
11020
+ }
11021
+ };
11022
+ }
11023
+ function definition(document, offset, page) {
11024
+ const found = symbolAt(document.getText(), offset, page);
11025
+ if (!found) return null;
11026
+ const { symbol } = found;
11027
+ if (symbol.kind === "handler" && page.controller) return definitionIn(page.controller, symbol.name);
11028
+ if (symbol.kind === "state" && page.state) return definitionIn(page.state, symbol.name);
11029
+ if (symbol.kind === "property" && symbol.shape.declaration) {
11030
+ const { file, line, character } = symbol.shape.declaration;
11031
+ const position = {
11032
+ line,
11033
+ character
11034
+ };
11035
+ return {
11036
+ uri: pathToFileURL(file).href,
11037
+ range: {
11038
+ start: position,
11039
+ end: position
11040
+ }
11041
+ };
11042
+ }
11043
+ if (symbol.kind === "static" && page.root) return locationIn(join(page.root, symbol.path), "", 0);
11044
+ return null;
11045
+ }
11046
+ function symbolAt(text, offset, page) {
11047
+ const context = markupContextAt(text, offset);
11048
+ switch (context.kind) {
11049
+ case "tagName":
11050
+ case "closeTag": {
11051
+ const { start, end } = wordAt(text, offset, TAG_NAME);
11052
+ return {
11053
+ symbol: {
11054
+ kind: "component",
11055
+ tag: text.slice(start, end)
11056
+ },
11057
+ start,
11058
+ end
11059
+ };
11060
+ }
11061
+ case "attributeName": {
11062
+ const { start, end } = wordAt(text, offset, ATTRIBUTE_NAME);
11063
+ const name = text.slice(start, end);
11064
+ if (!name) return null;
11065
+ return {
11066
+ symbol: {
11067
+ kind: "attribute",
11068
+ tag: context.element.tag,
11069
+ name
11070
+ },
11071
+ start,
11072
+ end
11073
+ };
11074
+ }
11075
+ case "attributeValue": {
11076
+ const { start, end } = wordAt(text, offset, VALUE);
11077
+ const value = text.slice(start, end);
11078
+ if (isEventProp(context.attribute)) return {
11079
+ symbol: {
11080
+ kind: "handler",
11081
+ name: value
11082
+ },
11083
+ start,
11084
+ end
11085
+ };
11086
+ if (page.statics?.includes(value)) return {
11087
+ symbol: {
11088
+ kind: "static",
11089
+ path: value
11090
+ },
11091
+ start,
11092
+ end
11093
+ };
11094
+ return null;
11095
+ }
11096
+ case "binding": {
11097
+ const { start, end } = wordAt(text, offset, IDENTIFIER);
11098
+ const name = text.slice(start, end);
11099
+ if (!name) return null;
11100
+ const scope = bindingScope(context.loopVariables, page);
11101
+ if (isMember(text, context.start, start)) {
11102
+ const owner = ownerOf(text, context.start, start, scope);
11103
+ if (!owner) return null;
11104
+ const shape = membersOf(owner)[name];
11105
+ if (!shape) return null;
11106
+ return {
11107
+ symbol: {
11108
+ kind: "property",
11109
+ name,
11110
+ owner,
11111
+ shape
11112
+ },
11113
+ start,
11114
+ end
11115
+ };
11116
+ }
11117
+ const variable = [...context.loopVariables].reverse().find((candidate) => candidate.name === name);
11118
+ if (variable) return {
11119
+ symbol: {
11120
+ kind: "loopVariable",
11121
+ variable,
11122
+ shape: scope?.loops.get(name)
11123
+ },
11124
+ start,
11125
+ end
11126
+ };
11127
+ return {
11128
+ symbol: {
11129
+ kind: "state",
11130
+ name
11131
+ },
11132
+ start,
11133
+ end
11134
+ };
11135
+ }
11136
+ default: return null;
11137
+ }
11138
+ }
11139
+ function describeSymbol(symbol, page, components) {
11140
+ switch (symbol.kind) {
11141
+ case "component": {
11142
+ const controlFlowDoc = CONTROL_FLOW_DOCS[symbol.tag];
11143
+ if (controlFlowDoc) return `**<${symbol.tag}>** · control flow\n\n${controlFlowDoc}`;
11144
+ const component = components[symbol.tag];
11145
+ if (!component) return null;
11146
+ return describeComponent(component);
11147
+ }
11148
+ case "attribute": return describeAttribute(symbol.tag, symbol.name, components);
11149
+ case "handler": {
11150
+ const { controller } = page;
11151
+ if (controller?.mutations?.includes(symbol.name)) return `mutation \`${symbol.name}\` in controller.ts`;
11152
+ if (controller?.actions?.includes(symbol.name)) return `action \`${symbol.name}\` in controller.ts`;
11153
+ return null;
11154
+ }
11155
+ case "state": {
11156
+ if (!page.state?.keys?.includes(symbol.name)) return null;
11157
+ const shape = page.state.shapes?.[symbol.name];
11158
+ return `state \`${symbol.name}\` in state.ts${typeSuffix(shape)}`;
11159
+ }
11160
+ case "loopVariable": return `\`${symbol.variable.name}\`: ${describeLoopVariable(symbol.variable, symbol.shape)}`;
11161
+ case "property": return `\`${symbol.name}\`: \`${symbol.shape.type}\`, a property of \`${symbol.owner.type}\``;
11162
+ case "static": return `image \`${symbol.path}\``;
11163
+ }
11164
+ }
11165
+ function describeAttribute(tag, name, components) {
11166
+ if (CONTROL_FLOW_TAGS[tag]) {
11167
+ const doc = CONTROL_FLOW_ATTRIBUTE_DOCS[name];
11168
+ if (!doc) return null;
11169
+ return `\`${name}\`: ${doc}`;
11170
+ }
11171
+ const component = components[tag];
11172
+ if (!component) return null;
11173
+ const type = component.props[name];
11174
+ if (type) return `\`${name}\`: ${describeType(type)}`;
11175
+ if (name.startsWith(DATA_PREFIX)) return `\`${name}\`: this element's handlers get it as \`event.data.${dataKey(name)}\``;
11176
+ if (isEventProp(name) && component.events.includes(eventName(name))) return `\`${name}\`: on \`${eventName(name)}\`, runs a mutation or action from controller.ts`;
11177
+ return null;
11178
+ }
11179
+ function definitionIn(source, name) {
11180
+ return locationIn(source.file, source.text, source.offsets[name]);
11181
+ }
11182
+ function locationIn(file, text, offset) {
11183
+ if (offset === void 0) return null;
11184
+ const position = TextDocument.create("", "typescript", 0, text).positionAt(offset);
11185
+ return {
11186
+ uri: pathToFileURL(file).href,
11187
+ range: {
11188
+ start: position,
11189
+ end: position
11190
+ }
11191
+ };
11192
+ }
11193
+ //#endregion
11156
11194
  //#region src/lsp/server.ts
11157
- const WATCHED = [
11195
+ const WATCHED_FILES = [
11158
11196
  "**/*.ts",
11159
11197
  "**/dot.json",
11160
11198
  "**/statics/**"
11161
11199
  ];
11162
- /**
11163
- * Serves .dml files over the Language Server Protocol: completion, hover, go to definition and
11164
- * the compiler's own errors and warnings. It runs from the dot's own installed `dotctl`, so what
11165
- * it knows always matches the Dot version the dot builds with.
11166
- */
11167
11200
  function startLanguageServer(connection) {
11168
11201
  const documents = new import_main.TextDocuments(TextDocument);
11169
- const features = { components: builtinRegistry };
11202
+ const components = builtinRegistry;
11170
11203
  const stateTypes = new StateTypes();
11171
11204
  let canWatchFiles = false;
11172
11205
  const readText = async (file) => {
11173
- const open = documents.get(pathToFileURL(file).href);
11174
- if (open) return open.getText();
11175
- return existsSync(file) ? readFile(file, "utf8") : void 0;
11206
+ const openDocument = documents.get(pathToFileURL(file).href);
11207
+ if (openDocument) return openDocument.getText();
11208
+ if (!existsSync(file)) return;
11209
+ return readFile(file, "utf8");
11176
11210
  };
11177
- const pageOf = async (uri) => {
11211
+ const openView = async (uri) => {
11178
11212
  const document = documents.get(uri);
11179
11213
  if (!document || !uri.endsWith(".dml")) return;
11180
11214
  const file = fileURLToPath(uri);
11181
11215
  return {
11182
11216
  document,
11183
- page: await readPage(file, readText, stateTypes),
11184
- file
11217
+ file,
11218
+ page: await readPage(file, readText, stateTypes)
11185
11219
  };
11186
11220
  };
11187
11221
  const validate = async (uri) => {
11188
11222
  try {
11189
- const found = await pageOf(uri);
11190
- if (!found) return;
11191
- const diagnostics = diagnose(found.document, found.file, found.page, features);
11223
+ const view = await openView(uri);
11224
+ if (!view) return;
11225
+ const diagnostics = diagnose(view.document, view.file, view.page, components);
11192
11226
  await connection.sendDiagnostics({
11193
11227
  uri,
11194
- version: found.document.version,
11228
+ version: view.document.version,
11195
11229
  diagnostics
11196
11230
  });
11197
11231
  } catch (error) {
@@ -11220,10 +11254,14 @@ function startLanguageServer(connection) {
11220
11254
  };
11221
11255
  });
11222
11256
  connection.onInitialized(() => {
11223
- if (canWatchFiles) connection.client.register(import_main.DidChangeWatchedFilesNotification.type, { watchers: WATCHED.map((globPattern) => ({ globPattern })) });
11257
+ if (canWatchFiles) {
11258
+ const watchers = WATCHED_FILES.map((globPattern) => ({ globPattern }));
11259
+ connection.client.register(import_main.DidChangeWatchedFilesNotification.type, { watchers });
11260
+ }
11224
11261
  });
11225
11262
  documents.onDidChangeContent(({ document }) => {
11226
- document.uri.endsWith(".dml") ? validate(document.uri) : validateAll();
11263
+ if (document.uri.endsWith(".dml")) validate(document.uri);
11264
+ else validateAll();
11227
11265
  });
11228
11266
  documents.onDidClose(({ document }) => {
11229
11267
  connection.sendDiagnostics({
@@ -11234,30 +11272,37 @@ function startLanguageServer(connection) {
11234
11272
  connection.onDidChangeWatchedFiles(() => {
11235
11273
  validateAll();
11236
11274
  });
11237
- connection.onCompletion(async ({ textDocument, position }) => {
11238
- const found = await pageOf(textDocument.uri);
11239
- if (!found) return [];
11240
- return complete(found.document, found.document.offsetAt(position), found.page, features);
11275
+ const openViewAt = async ({ textDocument, position }) => {
11276
+ const view = await openView(textDocument.uri);
11277
+ if (!view) return;
11278
+ return {
11279
+ ...view,
11280
+ offset: view.document.offsetAt(position)
11281
+ };
11282
+ };
11283
+ connection.onCompletion(async (request) => {
11284
+ const view = await openViewAt(request);
11285
+ if (!view) return [];
11286
+ return complete(view.document, view.offset, view.page, components);
11241
11287
  });
11242
- connection.onHover(async ({ textDocument, position }) => {
11243
- const found = await pageOf(textDocument.uri);
11244
- if (!found) return null;
11245
- return hover(found.document, found.document.offsetAt(position), found.page, features);
11288
+ connection.onHover(async (request) => {
11289
+ const view = await openViewAt(request);
11290
+ if (!view) return null;
11291
+ return hover(view.document, view.offset, view.page, components);
11246
11292
  });
11247
- connection.onDefinition(async ({ textDocument, position }) => {
11248
- const found = await pageOf(textDocument.uri);
11249
- if (!found) return null;
11250
- return definition(found.document, found.document.offsetAt(position), found.page);
11293
+ connection.onDefinition(async (request) => {
11294
+ const view = await openViewAt(request);
11295
+ if (!view) return null;
11296
+ return definition(view.document, view.offset, view.page);
11251
11297
  });
11252
11298
  documents.listen(connection);
11253
11299
  connection.listen();
11254
11300
  return connection;
11255
11301
  }
11256
- /** Serves an editor that started `dotctl lsp`, over stdin and stdout. */
11257
11302
  function serveStdio() {
11258
11303
  return startLanguageServer((0, import_node.createConnection)(import_node.ProposedFeatures.all, process.stdin, process.stdout));
11259
11304
  }
11260
11305
  //#endregion
11261
11306
  export { serveStdio };
11262
11307
 
11263
- //# sourceMappingURL=server-DFOUQnLA.js.map
11308
+ //# sourceMappingURL=server-DMnWDdJ6.js.map