@monimesl/dot-cli 0.1.0-alpha.19 → 0.1.0-alpha.21

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 (58) hide show
  1. package/dist/cli.js +381 -261
  2. package/dist/cli.js.map +1 -1
  3. package/dist/{server-uIyYmYnp.js → server-B9-F93mS.js} +539 -464
  4. package/dist/server-B9-F93mS.js.map +1 -0
  5. package/dist/templates/showcase/dot.ts +2 -0
  6. package/dist/templates/showcase/pages/basics/bars/controller.ts +1 -1
  7. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +1 -1
  8. package/dist/templates/showcase/pages/basics/events/controller.ts +4 -4
  9. package/dist/templates/showcase/pages/basics/events/view.dml +2 -2
  10. package/dist/templates/showcase/pages/basics/hooks/controller.ts +6 -0
  11. package/dist/templates/showcase/pages/basics/hooks/view.dml +5 -0
  12. package/dist/templates/showcase/pages/basics/navigation/controller.ts +7 -0
  13. package/dist/templates/showcase/pages/basics/navigation/state.ts +2 -0
  14. package/dist/templates/showcase/pages/basics/navigation/view.dml +11 -0
  15. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +12 -0
  16. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  17. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +9 -0
  18. package/dist/templates/showcase/pages/components/carousel/controller.ts +1 -1
  19. package/dist/templates/showcase/pages/components/feedback/controller.ts +12 -2
  20. package/dist/templates/showcase/pages/components/feedback/state.ts +33 -0
  21. package/dist/templates/showcase/pages/components/feedback/view.dml +21 -1
  22. package/dist/templates/showcase/pages/components/forms/controller.ts +22 -22
  23. package/dist/templates/showcase/pages/components/forms/state.ts +3 -3
  24. package/dist/templates/showcase/pages/components/forms/view.dml +9 -2
  25. package/dist/templates/showcase/pages/components/icons/controller.ts +1 -1
  26. package/dist/templates/showcase/pages/components/images/controller.ts +3 -1
  27. package/dist/templates/showcase/pages/components/images/view.dml +13 -0
  28. package/dist/templates/showcase/pages/components/layout/controller.ts +2 -2
  29. package/dist/templates/showcase/pages/components/lists/controller.ts +1 -1
  30. package/dist/templates/showcase/pages/components/lists/view.dml +18 -0
  31. package/dist/templates/showcase/pages/components/money/controller.ts +6 -8
  32. package/dist/templates/showcase/pages/components/sheets/controller.ts +2 -2
  33. package/dist/templates/showcase/pages/components/tabs/controller.ts +22 -10
  34. package/dist/templates/showcase/pages/components/tabs/state.ts +1 -0
  35. package/dist/templates/showcase/pages/components/tabs/view.dml +4 -3
  36. package/dist/templates/showcase/pages/dash/cache/controller.ts +2 -2
  37. package/dist/templates/showcase/pages/dash/calls/controller.ts +7 -6
  38. package/dist/templates/showcase/pages/dash/calls/state.ts +1 -1
  39. package/dist/templates/showcase/pages/dash/calls/view.dml +3 -2
  40. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  41. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  42. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  43. package/dist/templates/showcase/pages/dash/files/controller.ts +15 -13
  44. package/dist/templates/showcase/pages/dash/files/state.ts +3 -6
  45. package/dist/templates/showcase/pages/dash/media/controller.ts +16 -13
  46. package/dist/templates/showcase/pages/dash/network/controller.ts +71 -16
  47. package/dist/templates/showcase/pages/dash/network/state.ts +10 -0
  48. package/dist/templates/showcase/pages/dash/network/view.dml +32 -4
  49. package/dist/templates/showcase/pages/dash/secrets/controller.ts +1 -1
  50. package/dist/templates/showcase/pages/dash/system/controller.ts +32 -5
  51. package/dist/templates/showcase/pages/dash/system/state.ts +6 -2
  52. package/dist/templates/showcase/pages/dash/system/view.dml +39 -1
  53. package/dist/templates/showcase/pages/dash/ui/controller.ts +23 -23
  54. package/dist/templates/showcase/pages/dash/ui/view.dml +1 -1
  55. package/dist/templates/showcase/pages/index/controller.ts +1 -1
  56. package/dist/templates/showcase/pages/index/state.ts +17 -10
  57. package/package.json +6 -6
  58. package/dist/server-uIyYmYnp.js.map +0 -1
@@ -10144,161 +10144,57 @@ function getWellformedEdit(textEdit) {
10144
10144
  return textEdit;
10145
10145
  }
10146
10146
  //#endregion
10147
- //#region src/lsp/shapes.ts
10148
- const NUMBER = {
10149
- kind: "value",
10150
- 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."
10151
10153
  };
10152
- function parseBinding(source) {
10153
- try {
10154
- return parseExpression(source);
10155
- } catch (error) {
10156
- if (error instanceof ScriptParseError) return;
10157
- throw error;
10158
- }
10159
- }
10160
- function shapeOf(expr, scope) {
10161
- switch (expr.type) {
10162
- case "Identifier":
10163
- if (scope.loops.has(expr.name)) return scope.loops.get(expr.name);
10164
- return scope.state[expr.name];
10165
- case "Member": {
10166
- const owner = shapeOf(expr.object, scope);
10167
- if (!owner) return;
10168
- const { property } = expr;
10169
- if (property.type === "Literal" && typeof property.value === "string") return membersOf(owner)[property.value];
10170
- return owner.kind === "list" ? owner.item : void 0;
10171
- }
10172
- default: return;
10173
- }
10174
- }
10175
- function membersOf(shape) {
10176
- switch (shape.kind) {
10177
- case "object": return shape.properties;
10178
- case "list": return { length: NUMBER };
10179
- 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(", ")}`);
10180
10168
  }
10169
+ return lines.join("\n");
10181
10170
  }
10182
- function describeMembers(shape) {
10183
- return Object.keys(membersOf(shape)).join(", ") || "none";
10184
- }
10185
- function loopScope(variables, state) {
10186
- const loops = /* @__PURE__ */ new Map();
10187
- for (const variable of variables) if (variable.role === "index") loops.set(variable.name, NUMBER);
10188
- else loops.set(variable.name, itemShape(variable.list, {
10189
- state,
10190
- loops
10191
- }));
10192
- return {
10193
- state,
10194
- loops
10195
- };
10196
- }
10197
- function itemShape(each, scope) {
10198
- const list = parseBinding(each.replace(/^\s*{{|}}\s*$/g, ""));
10199
- const shape = list && shapeOf(list, scope);
10200
- return shape?.kind === "list" ? shape.item : void 0;
10201
- }
10202
- function withLoop(scope, item, itemShape, index) {
10203
- const loops = new Map(scope.loops).set(item, itemShape).set(index, NUMBER);
10204
- return {
10205
- state: scope.state,
10206
- loops
10207
- };
10208
- }
10209
- //#endregion
10210
- //#region src/lsp/check-members.ts
10211
- const BINDING = /{{([\s\S]*?)}}/g;
10212
- function checkMembers(text, state) {
10213
- let nodes;
10214
- try {
10215
- nodes = parseDmlToIR(text, {
10216
- locations: true,
10217
- whitespace: "preserve"
10218
- });
10219
- } catch {
10220
- return [];
10221
- }
10222
- const problems = [];
10223
- checkNodes(text, nodes, {
10224
- state,
10225
- loops: /* @__PURE__ */ new Map()
10226
- }, problems);
10227
- 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(" | ");
10228
10175
  }
10229
- function checkNodes(text, nodes, scope, problems) {
10230
- for (const node of nodes) {
10231
- const from = node.loc?.offset ?? 0;
10232
- if (node.type === "Text") {
10233
- checkBindings(text, node.content, from, scope, problems);
10234
- continue;
10235
- }
10236
- for (const value of Object.values(node.attributes)) checkBindings(text, value, from, scope, problems);
10237
- if (node.tag !== "For") {
10238
- checkNodes(text, node.children, scope, problems);
10239
- continue;
10240
- }
10241
- const { each = "", item = "item", index = "index", key } = node.attributes;
10242
- const itemOfList = itemShape(each, scope);
10243
- if (itemOfList?.kind === "object" && key !== void 0 && !key.startsWith("*") && !(key in itemOfList.properties)) problems.push({
10244
- message: `The <For> \`key\` "${key}" is not a property of \`${itemOfList.type}\`. It has: ${describeMembers(itemOfList)}`,
10245
- offset: offsetOf(text, `key="${key}"`, from)
10246
- });
10247
- checkNodes(text, node.children, withLoop(scope, item, itemOfList, index), problems);
10248
- }
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}`;
10249
10180
  }
10250
- function checkBindings(text, value, from, scope, problems) {
10251
- for (const [binding, source = ""] of value.matchAll(BINDING)) {
10252
- const expr = parseBinding(source);
10253
- if (!expr) continue;
10254
- const bindingOffset = offsetOf(text, binding, from);
10255
- for (const { name, owner } of unknownMembers(expr, scope)) problems.push({
10256
- message: `\`${name}\` is not a property of \`${owner.type}\`. It has: ${describeMembers(owner)}`,
10257
- offset: memberOffset(bindingOffset, binding, name)
10258
- });
10259
- }
10181
+ function typeSuffix(shape) {
10182
+ return shape ? `: \`${shape.type}\`` : "";
10260
10183
  }
10261
- function unknownMembers(expr, scope) {
10262
- const found = (node) => unknownMembers(node, scope);
10263
- switch (expr.type) {
10264
- case "Literal":
10265
- case "Identifier": return [];
10266
- case "Member": {
10267
- const inner = [...found(expr.object), ...found(expr.property)];
10268
- const { property } = expr;
10269
- if (property.type !== "Literal" || typeof property.value !== "string") return inner;
10270
- const owner = shapeOf(expr.object, scope);
10271
- if (owner && owner.kind !== "value" && !(property.value in membersOf(owner))) return [...inner, {
10272
- name: property.value,
10273
- owner
10274
- }];
10275
- return inner;
10276
- }
10277
- case "Unary": return found(expr.argument);
10278
- case "Binary": return [...found(expr.left), ...found(expr.right)];
10279
- case "Conditional": return [
10280
- ...found(expr.test),
10281
- ...found(expr.consequent),
10282
- ...found(expr.alternate)
10283
- ];
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";
10284
10189
  }
10285
10190
  }
10286
- function offsetOf(text, written, from) {
10287
- const at = text.indexOf(written, from);
10288
- return at < 0 ? from : at;
10289
- }
10290
- function memberOffset(bindingOffset, binding, name) {
10291
- const escapedName = name.replaceAll("$", "\\$");
10292
- const match = new RegExp(`(?<=\\.\\s*)${escapedName}(?![\\w$])`).exec(binding);
10293
- return match ? bindingOffset + match.index : bindingOffset;
10294
- }
10295
10191
  //#endregion
10296
10192
  //#region src/lsp/markup.ts
10297
10193
  const TAG_NAME = /[A-Za-z0-9_.:-]/;
10298
- const ATTRIBUTE_NAME_END = /[\s=/>"']/;
10194
+ const ATTRIBUTE_NAME = /[^\s=/>"']/;
10299
10195
  const WHITESPACE = /\s/;
10300
10196
  function markupContextAt(text, offset) {
10301
- const open = [];
10197
+ const openElements = [];
10302
10198
  let index = 0;
10303
10199
  while (index < offset) if (text.startsWith("<!--", index)) {
10304
10200
  const end = text.indexOf("-->", index + 4);
@@ -10311,34 +10207,34 @@ function markupContextAt(text, offset) {
10311
10207
  if (offset <= nameEnd || close < 0 || close >= offset) return {
10312
10208
  kind: "closeTag",
10313
10209
  start: nameStart,
10314
- open: open.at(-1)
10210
+ open: openElements.at(-1)
10315
10211
  };
10316
- closeElement(open, text.slice(nameStart, nameEnd));
10212
+ closeElement(openElements, text.slice(nameStart, nameEnd));
10317
10213
  index = close + 1;
10318
10214
  } else if (text[index] === "<") {
10319
- const tag = readTag(text, index, offset, open);
10215
+ const tag = readTag(text, index, offset, openElements);
10320
10216
  if (tag.context) return tag.context;
10321
- if (tag.element && !tag.selfClosing) open.push(tag.element);
10217
+ if (tag.element && !tag.selfClosing) openElements.push(tag.element);
10322
10218
  index = tag.end;
10323
10219
  } else if (text.startsWith("{{", index)) {
10324
10220
  const end = text.indexOf("}}", index + 2);
10325
10221
  if (end < 0 || offset <= end) return {
10326
10222
  kind: "binding",
10327
10223
  start: index + 2,
10328
- loopVariables: loopVariables(open)
10224
+ loopVariables: loopVariables(openElements)
10329
10225
  };
10330
10226
  index = end + 2;
10331
10227
  } else index++;
10332
10228
  return { kind: "text" };
10333
10229
  }
10334
- function readTag(text, start, offset, open) {
10230
+ function readTag(text, start, offset, openElements) {
10335
10231
  const nameStart = start + 1;
10336
10232
  const nameEnd = readWhile(text, nameStart, TAG_NAME);
10337
10233
  if (offset <= nameEnd) return {
10338
10234
  context: {
10339
10235
  kind: "tagName",
10340
10236
  start: nameStart,
10341
- parents: [...open]
10237
+ parents: [...openElements]
10342
10238
  },
10343
10239
  end: nameEnd
10344
10240
  };
@@ -10373,7 +10269,7 @@ function readTag(text, start, offset, open) {
10373
10269
  end: index + 1
10374
10270
  };
10375
10271
  const attributeStart = index;
10376
- index = readUntil(text, index + 1, ATTRIBUTE_NAME_END);
10272
+ index = readWhile(text, index + 1, ATTRIBUTE_NAME);
10377
10273
  const name = text.slice(attributeStart, index);
10378
10274
  if (offset <= index) return {
10379
10275
  context: {
@@ -10393,7 +10289,7 @@ function readTag(text, start, offset, open) {
10393
10289
  if (quote === "\"" || quote === "'") {
10394
10290
  const close = text.indexOf(quote, valueStart + 1);
10395
10291
  if (close < 0 || offset <= close) return {
10396
- 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),
10397
10293
  end: offset
10398
10294
  };
10399
10295
  attributes[name] = text.slice(valueStart + 1, close);
@@ -10406,7 +10302,7 @@ function readTag(text, start, offset, open) {
10406
10302
  start: valueStart,
10407
10303
  element,
10408
10304
  attribute: name,
10409
- loopVariables: loopVariables(open)
10305
+ loopVariables: loopVariables(openElements)
10410
10306
  },
10411
10307
  end: valueEnd
10412
10308
  };
@@ -10415,8 +10311,8 @@ function readTag(text, start, offset, open) {
10415
10311
  }
10416
10312
  }
10417
10313
  }
10418
- function valueContext(valueBeforeCursor, valueStart, element, attribute, open) {
10419
- const variables = loopVariables(open);
10314
+ function valueContext(valueBeforeCursor, valueStart, element, attribute, openElements) {
10315
+ const variables = loopVariables(openElements);
10420
10316
  const bindingStart = valueBeforeCursor.lastIndexOf("{{");
10421
10317
  if (bindingStart >= 0 && !valueBeforeCursor.includes("}}", bindingStart)) return {
10422
10318
  kind: "binding",
@@ -10431,18 +10327,18 @@ function valueContext(valueBeforeCursor, valueStart, element, attribute, open) {
10431
10327
  loopVariables: variables
10432
10328
  };
10433
10329
  }
10434
- function closeElement(open, tag) {
10435
- for (let index = open.length - 1; index >= 0; index--) if (open[index].tag === tag) {
10436
- 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;
10437
10333
  return;
10438
10334
  }
10439
10335
  }
10440
- function loopVariables(open) {
10336
+ function loopVariables(openElements) {
10441
10337
  const variables = [];
10442
- for (const element of open) {
10338
+ for (const element of openElements) {
10443
10339
  if (element.tag !== "For") continue;
10444
10340
  const { item = "item", index = "index", each = "" } = element.attributes;
10445
- const list = each.replace(/^\s*{{\s*|\s*}}\s*$/g, "");
10341
+ const list = bindingSource(each);
10446
10342
  variables.push({
10447
10343
  name: item,
10448
10344
  role: "item",
@@ -10455,6 +10351,9 @@ function loopVariables(open) {
10455
10351
  }
10456
10352
  return variables;
10457
10353
  }
10354
+ function bindingSource(value) {
10355
+ return value.replace(/^\s*{{\s*|\s*}}\s*$/g, "");
10356
+ }
10458
10357
  function readWhile(text, index, pattern) {
10459
10358
  while (index < text.length && pattern.test(text[index])) index++;
10460
10359
  return index;
@@ -10472,78 +10371,161 @@ function wordAt(text, offset, pattern) {
10472
10371
  };
10473
10372
  }
10474
10373
  //#endregion
10475
- //#region src/lsp/features.ts
10476
- const ATTRIBUTE_NAME = /[^\s=/>"']/;
10477
- const IMAGE_ATTRIBUTES = ["src", "image"];
10478
- const IDENTIFIER = /[\w$]/;
10374
+ //#region src/lsp/shapes.ts
10375
+ const NUMBER_SHAPE = {
10376
+ kind: "value",
10377
+ type: "number"
10378
+ };
10479
10379
  const MEMBER_CHAIN = /([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*\.\s*$/;
10480
- 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
+ return Object.keys(membersOf(shape)).join(", ") || "none";
10413
+ }
10414
+ function loopScope(variables, state) {
10415
+ const loops = /* @__PURE__ */ new Map();
10416
+ for (const variable of variables) if (variable.role === "index") loops.set(variable.name, NUMBER_SHAPE);
10417
+ else loops.set(variable.name, itemShape(variable.list, {
10418
+ state,
10419
+ loops
10420
+ }));
10421
+ return {
10422
+ state,
10423
+ loops
10424
+ };
10425
+ }
10426
+ function bindingScope(loopVariables, page) {
10427
+ const shapes = page.state?.shapes;
10428
+ if (!shapes) return;
10429
+ return loopScope(loopVariables, shapes);
10430
+ }
10431
+ function itemShape(each, scope) {
10432
+ const list = parseBinding(bindingSource(each));
10433
+ if (!list) return;
10434
+ const shape = shapeOf(list, scope);
10435
+ if (shape?.kind === "list") return shape.item;
10436
+ }
10437
+ function withLoop(scope, item, itemOfList, index) {
10438
+ const loops = new Map(scope.loops);
10439
+ loops.set(item, itemOfList);
10440
+ loops.set(index, NUMBER_SHAPE);
10441
+ return {
10442
+ state: scope.state,
10443
+ loops
10444
+ };
10445
+ }
10446
+ function isMember(text, bindingStart, wordStart) {
10447
+ return text.slice(bindingStart, wordStart).trimEnd().endsWith(".");
10448
+ }
10449
+ function ownerOf(text, bindingStart, wordStart, scope) {
10450
+ const chain = MEMBER_CHAIN.exec(text.slice(bindingStart, wordStart))?.[1];
10451
+ if (chain === void 0) return;
10452
+ const expression = parseBinding(chain);
10453
+ if (!expression || !scope) return;
10454
+ return shapeOf(expression, scope);
10455
+ }
10456
+ //#endregion
10457
+ //#region src/lsp/completion.ts
10458
+ const IMAGE_ATTRIBUTES = ["src", "image"];
10459
+ const IDENTIFIER_BEFORE_CURSOR = /[A-Za-z_$][\w$]*$/;
10481
10460
  const CONTROL_FLOW_SNIPPETS = {
10482
10461
  If: "If condition=\"{{ $1 }}\">$0</If>",
10483
10462
  ElseIf: "ElseIf condition=\"{{ $1 }}\">$0</ElseIf>",
10484
10463
  Else: "Else>$0</Else>",
10485
10464
  For: "For each=\"{{ $1 }}\" key=\"$2\">$0</For>"
10486
10465
  };
10487
- const CONTROL_FLOW_DOCS = {
10488
- If: "Shows its children when `condition` is true.",
10489
- ElseIf: "Goes after an `<If>` or `<ElseIf>`. Shows its children when every condition before it is false and `condition` is true.",
10490
- Else: "Goes after an `<If>` or `<ElseIf>`. Shows its children when every condition before it is false.",
10491
- 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."
10492
- };
10493
- const CONTROL_FLOW_ATTRIBUTE_DOCS = {
10494
- condition: "A single `{{ }}` binding",
10495
- each: "The list to repeat, as a single `{{ }}` binding",
10496
- item: "The name for each item (default `item`)",
10497
- index: "The name for each position (default `index`)",
10498
- key: "A property of each item, `*this` or `*index`"
10499
- };
10500
- function complete(document, offset, page, { components }) {
10466
+ function complete(document, offset, page, components) {
10501
10467
  const text = document.getText();
10502
10468
  const context = markupContextAt(text, offset);
10503
- const range = (start) => ({
10504
- start: document.positionAt(start),
10505
- end: document.positionAt(offset)
10506
- });
10507
- const replacing = (start, items) => items.map((item) => ({
10508
- ...item,
10509
- textEdit: {
10510
- range: range(start),
10511
- newText: item.insertText ?? item.label
10512
- }
10513
- }));
10514
10469
  switch (context.kind) {
10515
- case "tagName": return replacing(context.start, tagCompletions(context.parents, components));
10470
+ case "tagName": {
10471
+ const items = tagCompletions(context.parents, components);
10472
+ return replacingFrom(document, context.start, offset, items);
10473
+ }
10516
10474
  case "closeTag":
10517
10475
  if (!context.open) return [];
10518
10476
  return [{
10519
10477
  label: `/${context.open.tag}`,
10520
10478
  kind: import_main.CompletionItemKind.Property,
10521
10479
  textEdit: {
10522
- range: range(context.start),
10480
+ range: rangeFrom(document, context.start, offset),
10523
10481
  newText: `${context.open.tag}>`
10524
10482
  }
10525
10483
  }];
10526
- case "attributeName": return replacing(context.start, attributeCompletions(context.element, components));
10484
+ case "attributeName": {
10485
+ const items = attributeCompletions(context.element, components);
10486
+ return replacingFrom(document, context.start, offset, items);
10487
+ }
10527
10488
  case "attributeValue": {
10528
- const scope = shapeScope(context.loopVariables, page);
10529
- return replacing(context.start, valueCompletions(context.element, context.attribute, page, scope, components));
10489
+ const scope = bindingScope(context.loopVariables, page);
10490
+ const items = valueCompletions(context.element, context.attribute, page, scope, components);
10491
+ return replacingFrom(document, context.start, offset, items);
10530
10492
  }
10531
10493
  case "binding": {
10532
10494
  const typed = text.slice(context.start, offset);
10533
- const wordStart = offset - (/[A-Za-z_$][\w$]*$/.exec(typed)?.[0] ?? "").length;
10534
- const scope = shapeScope(context.loopVariables, page);
10535
- return replacing(wordStart, isMember(text, context.start, wordStart) ? memberCompletions(ownerOf(text, context.start, wordStart, scope)) : bindingCompletions(context.loopVariables, page, scope));
10495
+ const wordStart = offset - (IDENTIFIER_BEFORE_CURSOR.exec(typed)?.[0] ?? "").length;
10496
+ const scope = bindingScope(context.loopVariables, page);
10497
+ let items;
10498
+ if (isMember(text, context.start, wordStart)) items = memberCompletions(ownerOf(text, context.start, wordStart, scope));
10499
+ else items = bindingCompletions(context.loopVariables, page, scope);
10500
+ return replacingFrom(document, wordStart, offset, items);
10536
10501
  }
10537
10502
  default: return [];
10538
10503
  }
10539
10504
  }
10505
+ function rangeFrom(document, start, offset) {
10506
+ return {
10507
+ start: document.positionAt(start),
10508
+ end: document.positionAt(offset)
10509
+ };
10510
+ }
10511
+ function replacingFrom(document, start, offset, items) {
10512
+ return items.map((item) => ({
10513
+ ...item,
10514
+ textEdit: {
10515
+ range: rangeFrom(document, start, offset),
10516
+ newText: item.insertText ?? item.label
10517
+ }
10518
+ }));
10519
+ }
10540
10520
  function tagCompletions(parents, components) {
10541
10521
  if (parents.length === 0) {
10542
10522
  const root = components[ROOT_TAG];
10543
- return root ? [componentItem(root, "0")] : [];
10523
+ if (!root) return [];
10524
+ return [componentItem(root, "0")];
10544
10525
  }
10545
10526
  const parent = [...parents].reverse().find((element) => !(element.tag in CONTROL_FLOW_TAGS));
10546
- const parentManifest = parent && components[parent.tag];
10527
+ let parentManifest;
10528
+ if (parent) parentManifest = components[parent.tag];
10547
10529
  const items = Object.values(components).filter((component) => !parentManifest || canHaveChildren(parentManifest, component)).map((component) => componentItem(component, "1"));
10548
10530
  for (const [tag, snippet] of Object.entries(CONTROL_FLOW_SNIPPETS)) items.push({
10549
10531
  label: tag,
@@ -10560,7 +10542,8 @@ function tagCompletions(parents, components) {
10560
10542
  return items;
10561
10543
  }
10562
10544
  function componentItem(component, sortGroup) {
10563
- const snippet = component.children === "none" ? `${component.name}$1 />` : `${component.name}$1>$0</${component.name}>`;
10545
+ let snippet = `${component.name}$1>$0</${component.name}>`;
10546
+ if (component.children === "none") snippet = `${component.name}$1 />`;
10564
10547
  return {
10565
10548
  label: component.name,
10566
10549
  kind: import_main.CompletionItemKind.Class,
@@ -10575,9 +10558,9 @@ function componentItem(component, sortGroup) {
10575
10558
  };
10576
10559
  }
10577
10560
  function attributeCompletions(element, components) {
10578
- const written = (name) => name in element.attributes;
10561
+ const isWritten = (name) => name in element.attributes;
10579
10562
  const controlFlow = CONTROL_FLOW_TAGS[element.tag];
10580
- if (controlFlow) return controlFlow.attributes.filter((name) => !written(name)).map((name) => ({
10563
+ if (controlFlow) return controlFlow.attributes.filter((name) => !isWritten(name)).map((name) => ({
10581
10564
  label: name,
10582
10565
  kind: import_main.CompletionItemKind.Property,
10583
10566
  detail: CONTROL_FLOW_ATTRIBUTE_DOCS[name] ?? name,
@@ -10586,7 +10569,7 @@ function attributeCompletions(element, components) {
10586
10569
  }));
10587
10570
  const component = components[element.tag];
10588
10571
  if (!component) return [];
10589
- const props = Object.entries(component.props).filter(([name]) => !written(name)).map(([name, type]) => ({
10572
+ const props = Object.entries(component.props).filter(([name]) => !isWritten(name)).map(([name, type]) => ({
10590
10573
  label: name,
10591
10574
  kind: import_main.CompletionItemKind.Property,
10592
10575
  detail: describeType(type),
@@ -10594,7 +10577,7 @@ function attributeCompletions(element, components) {
10594
10577
  insertTextFormat: import_main.InsertTextFormat.Snippet,
10595
10578
  sortText: `0${name}`
10596
10579
  }));
10597
- const events = component.events.map((event) => eventProp(event)).filter((name) => !written(name)).map((name) => ({
10580
+ const events = component.events.map((event) => eventProp(event)).filter((name) => !isWritten(name)).map((name) => ({
10598
10581
  label: name,
10599
10582
  kind: import_main.CompletionItemKind.Event,
10600
10583
  detail: "a mutation or action in controller.ts",
@@ -10605,25 +10588,10 @@ function attributeCompletions(element, components) {
10605
10588
  return [...props, ...events];
10606
10589
  }
10607
10590
  function valueCompletions(element, attribute, page, scope, components) {
10608
- if (element.tag === "For" && attribute === "key") {
10609
- const item = scope && itemShape(element.attributes.each ?? "", scope);
10610
- return [
10611
- ...item?.kind === "object" ? memberCompletions(item) : [],
10612
- {
10613
- label: "*this",
10614
- kind: import_main.CompletionItemKind.Value,
10615
- detail: "the item itself"
10616
- },
10617
- {
10618
- label: "*index",
10619
- kind: import_main.CompletionItemKind.Value,
10620
- detail: "match items by position"
10621
- }
10622
- ];
10623
- }
10591
+ if (element.tag === "For" && attribute === "key") return forKeyCompletions(element, scope);
10624
10592
  const component = components[element.tag];
10625
10593
  if (!component) return [];
10626
- if (isEventProp(attribute) && component.events.includes(eventName(attribute))) return handlerItems(page);
10594
+ if (isEventProp(attribute) && component.events.includes(eventName(attribute))) return handlerCompletions(page);
10627
10595
  const type = component.props[attribute];
10628
10596
  if (type && isWordsOr(type)) return type.words.map((value) => ({
10629
10597
  label: value,
@@ -10643,27 +10611,52 @@ function valueCompletions(element, attribute, page, scope, components) {
10643
10611
  }));
10644
10612
  return [];
10645
10613
  }
10646
- function handlerItems(page) {
10614
+ function forKeyCompletions(element, scope) {
10615
+ let properties = [];
10616
+ if (scope) {
10617
+ const item = itemShape(element.attributes.each ?? "", scope);
10618
+ if (item?.kind === "object") properties = memberCompletions(item);
10619
+ }
10620
+ return [
10621
+ ...properties,
10622
+ {
10623
+ label: "*this",
10624
+ kind: import_main.CompletionItemKind.Value,
10625
+ detail: "the item itself"
10626
+ },
10627
+ {
10628
+ label: "*index",
10629
+ kind: import_main.CompletionItemKind.Value,
10630
+ detail: "match items by position"
10631
+ }
10632
+ ];
10633
+ }
10634
+ function handlerCompletions(page) {
10647
10635
  const { controller } = page;
10648
10636
  if (!controller) return [];
10649
- return [...(controller.mutations ?? []).map((name) => ({
10637
+ const mutations = (controller.mutations ?? []).map((name) => ({
10650
10638
  label: name,
10651
10639
  kind: import_main.CompletionItemKind.Method,
10652
10640
  detail: "mutation"
10653
- })), ...(controller.actions ?? []).map((name) => ({
10641
+ }));
10642
+ const actions = (controller.actions ?? []).map((name) => ({
10654
10643
  label: name,
10655
10644
  kind: import_main.CompletionItemKind.Function,
10656
10645
  detail: "action"
10657
- }))];
10646
+ }));
10647
+ return [...mutations, ...actions];
10658
10648
  }
10659
10649
  function bindingCompletions(loopVariables, page, scope) {
10660
10650
  const items = /* @__PURE__ */ new Map();
10661
- for (const key of page.state?.keys ?? []) items.set(key, {
10662
- label: key,
10663
- kind: import_main.CompletionItemKind.Field,
10664
- detail: scope?.state[key] ? `state: ${scope.state[key].type}` : "state",
10665
- sortText: `1${key}`
10666
- });
10651
+ for (const key of page.state?.keys ?? []) {
10652
+ const shape = scope?.state[key];
10653
+ items.set(key, {
10654
+ label: key,
10655
+ kind: import_main.CompletionItemKind.Field,
10656
+ detail: shape ? `state: ${shape.type}` : "state",
10657
+ sortText: `1${key}`
10658
+ });
10659
+ }
10667
10660
  for (const variable of loopVariables) items.set(variable.name, {
10668
10661
  label: variable.name,
10669
10662
  kind: import_main.CompletionItemKind.Variable,
@@ -10680,227 +10673,107 @@ function memberCompletions(owner) {
10680
10673
  detail: shape.type
10681
10674
  }));
10682
10675
  }
10683
- function shapeScope(loopVariables, page) {
10684
- const shapes = page.state?.shapes;
10685
- return shapes && loopScope(loopVariables, shapes);
10686
- }
10687
- function isMember(text, bindingStart, start) {
10688
- return text.slice(bindingStart, start).trimEnd().endsWith(".");
10689
- }
10690
- function ownerOf(text, bindingStart, start, scope) {
10691
- const chain = MEMBER_CHAIN.exec(text.slice(bindingStart, start))?.[1];
10692
- const expr = chain === void 0 ? void 0 : parseBinding(chain);
10693
- return expr && scope && shapeOf(expr, scope);
10694
- }
10695
- function hover(document, offset, page, { components }) {
10696
- const found = symbolAt(document.getText(), offset, page);
10697
- if (!found) return null;
10698
- const markdown = describeSymbol(found.symbol, page, components);
10699
- if (!markdown) return null;
10700
- return {
10701
- contents: {
10702
- kind: import_main.MarkupKind.Markdown,
10703
- value: markdown
10704
- },
10705
- range: {
10706
- start: document.positionAt(found.start),
10707
- end: document.positionAt(found.end)
10708
- }
10709
- };
10710
- }
10711
- function definition(document, offset, page) {
10712
- const found = symbolAt(document.getText(), offset, page);
10713
- if (!found) return null;
10714
- const { symbol } = found;
10715
- if (symbol.kind === "handler" && page.controller) return locationIn(page.controller.file, page.controller.text, page.controller.offsets[symbol.name]);
10716
- if (symbol.kind === "state" && page.state) return locationIn(page.state.file, page.state.text, page.state.offsets[symbol.name]);
10717
- if (symbol.kind === "property" && symbol.shape.declaration) {
10718
- const { file, line, character } = symbol.shape.declaration;
10719
- const position = {
10720
- line,
10721
- character
10722
- };
10723
- return {
10724
- uri: pathToFileURL(file).href,
10725
- range: {
10726
- start: position,
10727
- end: position
10728
- }
10729
- };
10676
+ //#endregion
10677
+ //#region src/lsp/check-members.ts
10678
+ const BINDING = /{{([\s\S]*?)}}/g;
10679
+ function checkMembers(text, state) {
10680
+ let nodes;
10681
+ try {
10682
+ nodes = parseDmlToIR(text, {
10683
+ locations: true,
10684
+ whitespace: "preserve"
10685
+ });
10686
+ } catch {
10687
+ return [];
10730
10688
  }
10731
- if (symbol.kind === "static" && page.root) return locationIn(join(page.root, symbol.path), "", 0);
10732
- return null;
10689
+ const problems = [];
10690
+ checkNodes(text, nodes, {
10691
+ state,
10692
+ loops: /* @__PURE__ */ new Map()
10693
+ }, problems);
10694
+ return problems;
10733
10695
  }
10734
- function symbolAt(text, offset, page) {
10735
- const context = markupContextAt(text, offset);
10736
- switch (context.kind) {
10737
- case "tagName":
10738
- case "closeTag": {
10739
- const { start, end } = wordAt(text, offset, TAG_NAME);
10740
- return {
10741
- symbol: {
10742
- kind: "component",
10743
- tag: text.slice(start, end)
10744
- },
10745
- start,
10746
- end
10747
- };
10748
- }
10749
- case "attributeName": {
10750
- const { start, end } = wordAt(text, offset, ATTRIBUTE_NAME);
10751
- const name = text.slice(start, end);
10752
- return name ? {
10753
- symbol: {
10754
- kind: "attribute",
10755
- tag: context.element.tag,
10756
- name
10757
- },
10758
- start,
10759
- end
10760
- } : null;
10761
- }
10762
- case "attributeValue": {
10763
- const { start, end } = wordAt(text, offset, VALUE);
10764
- const value = text.slice(start, end);
10765
- if (isEventProp(context.attribute)) return {
10766
- symbol: {
10767
- kind: "handler",
10768
- name: value
10769
- },
10770
- start,
10771
- end
10772
- };
10773
- if (page.statics?.includes(value)) return {
10774
- symbol: {
10775
- kind: "static",
10776
- path: value
10777
- },
10778
- start,
10779
- end
10780
- };
10781
- return null;
10696
+ function checkNodes(text, nodes, scope, problems) {
10697
+ for (const node of nodes) {
10698
+ const from = node.loc?.offset ?? 0;
10699
+ if (node.type === "Text") {
10700
+ checkBindings(text, node.content, from, scope, problems);
10701
+ continue;
10782
10702
  }
10783
- case "binding": {
10784
- const { start, end } = wordAt(text, offset, IDENTIFIER);
10785
- const name = text.slice(start, end);
10786
- if (!name) return null;
10787
- const scope = shapeScope(context.loopVariables, page);
10788
- if (isMember(text, context.start, start)) {
10789
- const owner = ownerOf(text, context.start, start, scope);
10790
- const shape = owner && membersOf(owner)[name];
10791
- return owner && shape ? {
10792
- symbol: {
10793
- kind: "property",
10794
- name,
10795
- owner,
10796
- shape
10797
- },
10798
- start,
10799
- end
10800
- } : null;
10801
- }
10802
- const variable = [...context.loopVariables].reverse().find((candidate) => candidate.name === name);
10803
- if (variable) return {
10804
- symbol: {
10805
- kind: "loopVariable",
10806
- variable,
10807
- shape: scope?.loops.get(name)
10808
- },
10809
- start,
10810
- end
10811
- };
10812
- return {
10813
- symbol: {
10814
- kind: "state",
10815
- name
10816
- },
10817
- start,
10818
- end
10819
- };
10703
+ for (const value of Object.values(node.attributes)) checkBindings(text, value, from, scope, problems);
10704
+ if (node.tag !== "For") {
10705
+ checkNodes(text, node.children, scope, problems);
10706
+ continue;
10820
10707
  }
10821
- default: return null;
10708
+ const { each = "", item = "item", index = "index", key } = node.attributes;
10709
+ const itemOfList = itemShape(each, scope);
10710
+ if (itemOfList?.kind === "object" && key !== void 0 && !key.startsWith("*") && !(key in itemOfList.properties)) problems.push({
10711
+ message: `The <For> \`key\` "${key}" is not a property of \`${itemOfList.type}\`. It has: ${describeMembers(itemOfList)}`,
10712
+ offset: offsetOf(text, `key="${key}"`, from)
10713
+ });
10714
+ checkNodes(text, node.children, withLoop(scope, item, itemOfList, index), problems);
10822
10715
  }
10823
10716
  }
10824
- function describeSymbol(symbol, page, components) {
10825
- switch (symbol.kind) {
10826
- case "component": {
10827
- const controlFlow = CONTROL_FLOW_DOCS[symbol.tag];
10828
- if (controlFlow) return `**<${symbol.tag}>** · control flow\n\n${controlFlow}`;
10829
- const component = components[symbol.tag];
10830
- return component ? describeComponent(component) : null;
10831
- }
10832
- case "attribute": {
10833
- if (CONTROL_FLOW_TAGS[symbol.tag]) {
10834
- const doc = CONTROL_FLOW_ATTRIBUTE_DOCS[symbol.name];
10835
- return doc ? `\`${symbol.name}\`: ${doc}` : null;
10836
- }
10837
- const component = components[symbol.tag];
10838
- if (!component) return null;
10839
- const type = component.props[symbol.name];
10840
- if (type) return `\`${symbol.name}\`: ${describeType(type)}`;
10841
- if (symbol.name.startsWith(DATA_PREFIX)) return `\`${symbol.name}\`: this element's handlers get it as \`event.data.${dataKey(symbol.name)}\``;
10842
- if (isEventProp(symbol.name) && component.events.includes(eventName(symbol.name))) return `\`${symbol.name}\`: on \`${eventName(symbol.name)}\`, runs a mutation or action from controller.ts`;
10843
- return null;
10844
- }
10845
- case "handler": {
10846
- const { controller } = page;
10847
- if (controller?.mutations?.includes(symbol.name)) return `mutation \`${symbol.name}\` in controller.ts`;
10848
- if (controller?.actions?.includes(symbol.name)) return `action \`${symbol.name}\` in controller.ts`;
10849
- return null;
10850
- }
10851
- case "state": return page.state?.keys?.includes(symbol.name) ? `state \`${symbol.name}\` in state.ts${typeSuffix(page.state.shapes?.[symbol.name])}` : null;
10852
- case "loopVariable": return `\`${symbol.variable.name}\`: ${describeLoopVariable(symbol.variable, symbol.shape)}`;
10853
- case "property": return `\`${symbol.name}\`: \`${symbol.shape.type}\`, a property of \`${symbol.owner.type}\``;
10854
- case "static": return `image \`${symbol.path}\``;
10717
+ function checkBindings(text, value, from, scope, problems) {
10718
+ for (const [binding, source = ""] of value.matchAll(BINDING)) {
10719
+ const expression = parseBinding(source);
10720
+ if (!expression) continue;
10721
+ const bindingOffset = offsetOf(text, binding, from);
10722
+ for (const { name, owner } of unknownMembers(expression, scope)) problems.push({
10723
+ message: `\`${name}\` is not a property of \`${owner.type}\`. It has: ${describeMembers(owner)}`,
10724
+ offset: memberOffset(bindingOffset, binding, name)
10725
+ });
10855
10726
  }
10856
10727
  }
10857
- function describeComponent(component) {
10858
- const lines = [`**<${component.name}>** · ${describeChildren(component)}`];
10859
- const props = Object.entries(component.props);
10860
- if (props.length > 0) lines.push("", ...props.map(([name, type]) => `- \`${name}\`: ${describeType(type)}`));
10861
- if (component.events.length > 0) lines.push("", `Events: ${component.events.map((event) => `\`${eventProp(event)}\``).join(", ")}`);
10862
- return lines.join("\n");
10863
- }
10864
- function describeChildren(component) {
10865
- switch (component.children) {
10866
- case "none": return "no children";
10867
- case "text": return "text and inline components";
10868
- case "elements": return "any components";
10728
+ function unknownMembers(expression, scope) {
10729
+ const unknownIn = (node) => unknownMembers(node, scope);
10730
+ switch (expression.type) {
10731
+ case "Literal":
10732
+ case "Identifier": return [];
10733
+ case "Member": {
10734
+ const inner = [...unknownIn(expression.object), ...unknownIn(expression.property)];
10735
+ const { property } = expression;
10736
+ if (property.type !== "Literal" || typeof property.value !== "string") return inner;
10737
+ const owner = shapeOf(expression.object, scope);
10738
+ if (owner && owner.kind !== "value" && !(property.value in membersOf(owner))) return [...inner, {
10739
+ name: property.value,
10740
+ owner
10741
+ }];
10742
+ return inner;
10743
+ }
10744
+ case "Unary": return unknownIn(expression.argument);
10745
+ case "Binary": return [...unknownIn(expression.left), ...unknownIn(expression.right)];
10746
+ case "Conditional": return [
10747
+ ...unknownIn(expression.test),
10748
+ ...unknownIn(expression.consequent),
10749
+ ...unknownIn(expression.alternate)
10750
+ ];
10869
10751
  }
10870
10752
  }
10871
- function describeType(type) {
10872
- if (typeof type === "string") return type;
10873
- if (isWordsOr(type)) return `${describeType(type.words)} | ${otherThanWords(type)}`;
10874
- return type.map((value) => `"${value}"`).join(" | ");
10875
- }
10876
- function describeLoopVariable(variable, shape) {
10877
- const list = variable.list ? ` \`${variable.list}\`` : "";
10878
- return variable.role === "item" ? `each item of${list}${typeSuffix(shape)}` : `the position in${list}`;
10879
- }
10880
- function typeSuffix(shape) {
10881
- return shape ? `: \`${shape.type}\`` : "";
10753
+ function offsetOf(text, written, from) {
10754
+ const at = text.indexOf(written, from);
10755
+ return at < 0 ? from : at;
10882
10756
  }
10883
- function locationIn(file, text, offset) {
10884
- if (offset === void 0) return null;
10885
- const position = TextDocument.create("", "typescript", 0, text).positionAt(offset);
10886
- return {
10887
- uri: pathToFileURL(file).href,
10888
- range: {
10889
- start: position,
10890
- end: position
10891
- }
10892
- };
10757
+ function memberOffset(bindingOffset, binding, name) {
10758
+ const escapedName = name.replaceAll("$", "\\$");
10759
+ const match = new RegExp(`(?<=\\.\\s*)${escapedName}(?![\\w$])`).exec(binding);
10760
+ return match ? bindingOffset + match.index : bindingOffset;
10893
10761
  }
10894
- function diagnose(document, file, page, { components }) {
10762
+ //#endregion
10763
+ //#region src/lsp/diagnostics.ts
10764
+ function diagnose(document, file, page, components) {
10895
10765
  const diagnostics = [];
10896
10766
  const context = {
10897
10767
  file,
10898
10768
  components,
10899
- onWarning: (warning) => diagnostics.push(sourceDiagnostic(document, warning, import_main.DiagnosticSeverity.Warning))
10769
+ onWarning: (warning) => {
10770
+ diagnostics.push(sourceDiagnostic(document, warning, import_main.DiagnosticSeverity.Warning));
10771
+ }
10900
10772
  };
10901
10773
  if (page.statics) context.statics = page.statics;
10902
10774
  if (page.state?.keys) context.stateKeys = page.state.keys;
10903
- const { mutations, actions } = page.controller ?? {};
10775
+ const mutations = page.controller?.mutations;
10776
+ const actions = page.controller?.actions;
10904
10777
  if (mutations && actions) context.handlers = [...mutations, ...actions];
10905
10778
  try {
10906
10779
  compileView(document.getText(), context);
@@ -10918,13 +10791,14 @@ function diagnosticAt(document, start, message, severity) {
10918
10791
  const text = document.getText();
10919
10792
  let end = start;
10920
10793
  while (end < text.length && !/[\s>]/.test(text[end])) end++;
10794
+ const atLeastOneCharacter = Math.min(start + 1, text.length);
10921
10795
  return {
10922
10796
  severity,
10923
10797
  source: "dot",
10924
10798
  message,
10925
10799
  range: {
10926
10800
  start: document.positionAt(start),
10927
- end: document.positionAt(Math.max(end, Math.min(start + 1, text.length)))
10801
+ end: document.positionAt(Math.max(end, atLeastOneCharacter))
10928
10802
  }
10929
10803
  };
10930
10804
  }
@@ -11001,13 +10875,13 @@ var StateTypes = class {
11001
10875
  const key = root ?? file;
11002
10876
  let project = this.#projects.get(key);
11003
10877
  if (!project) {
11004
- project = new Project(root);
10878
+ project = new TypeScriptProject(root);
11005
10879
  this.#projects.set(key, project);
11006
10880
  }
11007
10881
  return project.read(file, text);
11008
10882
  }
11009
10883
  };
11010
- var Project = class {
10884
+ var TypeScriptProject = class {
11011
10885
  #options;
11012
10886
  #fileNames;
11013
10887
  #open = /* @__PURE__ */ new Map();
@@ -11062,9 +10936,9 @@ var Project = class {
11062
10936
  function readConfig(root) {
11063
10937
  const file = join(root, "tsconfig.json");
11064
10938
  if (!existsSync(file)) return;
11065
- const read = ts.readConfigFile(file, (path) => ts.sys.readFile(path));
11066
- if (read.error) return;
11067
- return ts.parseJsonConfigFileContent(read.config, ts.sys, root, void 0, file);
10939
+ const configFile = ts.readConfigFile(file, (path) => ts.sys.readFile(path));
10940
+ if (configFile.error) return;
10941
+ return ts.parseJsonConfigFileContent(configFile.config, ts.sys, root, void 0, file);
11068
10942
  }
11069
10943
  function propertyShape(checker, property, depth, seen) {
11070
10944
  const shape = typeShape(checker, checker.getTypeOfSymbol(property), depth, seen);
@@ -11116,40 +10990,234 @@ function isRecord(checker, type) {
11116
10990
  return (type.isUnion() ? type.types : [type]).every((member) => (member.flags & ts.TypeFlags.Object) !== 0 && member.getCallSignatures().length === 0 && checker.getIndexInfosOfType(member).length === 0);
11117
10991
  }
11118
10992
  //#endregion
10993
+ //#region src/lsp/symbols.ts
10994
+ const IDENTIFIER = /[\w$]/;
10995
+ const VALUE = /[^\s"']/;
10996
+ function hover(document, offset, page, components) {
10997
+ const found = symbolAt(document.getText(), offset, page);
10998
+ if (!found) return null;
10999
+ const markdown = describeSymbol(found.symbol, page, components);
11000
+ if (!markdown) return null;
11001
+ return {
11002
+ contents: {
11003
+ kind: import_main.MarkupKind.Markdown,
11004
+ value: markdown
11005
+ },
11006
+ range: {
11007
+ start: document.positionAt(found.start),
11008
+ end: document.positionAt(found.end)
11009
+ }
11010
+ };
11011
+ }
11012
+ function definition(document, offset, page) {
11013
+ const found = symbolAt(document.getText(), offset, page);
11014
+ if (!found) return null;
11015
+ const { symbol } = found;
11016
+ if (symbol.kind === "handler" && page.controller) {
11017
+ const { file, text, offsets } = page.controller;
11018
+ return locationIn(file, text, offsets[symbol.name]);
11019
+ }
11020
+ if (symbol.kind === "state" && page.state) {
11021
+ const { file, text, offsets } = page.state;
11022
+ return locationIn(file, text, offsets[symbol.name]);
11023
+ }
11024
+ if (symbol.kind === "property" && symbol.shape.declaration) {
11025
+ const { file, line, character } = symbol.shape.declaration;
11026
+ const position = {
11027
+ line,
11028
+ character
11029
+ };
11030
+ return {
11031
+ uri: pathToFileURL(file).href,
11032
+ range: {
11033
+ start: position,
11034
+ end: position
11035
+ }
11036
+ };
11037
+ }
11038
+ if (symbol.kind === "static" && page.root) return locationIn(join(page.root, symbol.path), "", 0);
11039
+ return null;
11040
+ }
11041
+ function symbolAt(text, offset, page) {
11042
+ const context = markupContextAt(text, offset);
11043
+ switch (context.kind) {
11044
+ case "tagName":
11045
+ case "closeTag": {
11046
+ const { start, end } = wordAt(text, offset, TAG_NAME);
11047
+ return {
11048
+ symbol: {
11049
+ kind: "component",
11050
+ tag: text.slice(start, end)
11051
+ },
11052
+ start,
11053
+ end
11054
+ };
11055
+ }
11056
+ case "attributeName": {
11057
+ const { start, end } = wordAt(text, offset, ATTRIBUTE_NAME);
11058
+ const name = text.slice(start, end);
11059
+ if (!name) return null;
11060
+ return {
11061
+ symbol: {
11062
+ kind: "attribute",
11063
+ tag: context.element.tag,
11064
+ name
11065
+ },
11066
+ start,
11067
+ end
11068
+ };
11069
+ }
11070
+ case "attributeValue": {
11071
+ const { start, end } = wordAt(text, offset, VALUE);
11072
+ const value = text.slice(start, end);
11073
+ if (isEventProp(context.attribute)) return {
11074
+ symbol: {
11075
+ kind: "handler",
11076
+ name: value
11077
+ },
11078
+ start,
11079
+ end
11080
+ };
11081
+ if (page.statics?.includes(value)) return {
11082
+ symbol: {
11083
+ kind: "static",
11084
+ path: value
11085
+ },
11086
+ start,
11087
+ end
11088
+ };
11089
+ return null;
11090
+ }
11091
+ case "binding": {
11092
+ const { start, end } = wordAt(text, offset, IDENTIFIER);
11093
+ const name = text.slice(start, end);
11094
+ if (!name) return null;
11095
+ const scope = bindingScope(context.loopVariables, page);
11096
+ if (isMember(text, context.start, start)) {
11097
+ const owner = ownerOf(text, context.start, start, scope);
11098
+ if (!owner) return null;
11099
+ const shape = membersOf(owner)[name];
11100
+ if (!shape) return null;
11101
+ return {
11102
+ symbol: {
11103
+ kind: "property",
11104
+ name,
11105
+ owner,
11106
+ shape
11107
+ },
11108
+ start,
11109
+ end
11110
+ };
11111
+ }
11112
+ const variable = [...context.loopVariables].reverse().find((candidate) => candidate.name === name);
11113
+ if (variable) return {
11114
+ symbol: {
11115
+ kind: "loopVariable",
11116
+ variable,
11117
+ shape: scope?.loops.get(name)
11118
+ },
11119
+ start,
11120
+ end
11121
+ };
11122
+ return {
11123
+ symbol: {
11124
+ kind: "state",
11125
+ name
11126
+ },
11127
+ start,
11128
+ end
11129
+ };
11130
+ }
11131
+ default: return null;
11132
+ }
11133
+ }
11134
+ function describeSymbol(symbol, page, components) {
11135
+ switch (symbol.kind) {
11136
+ case "component": {
11137
+ const controlFlowDoc = CONTROL_FLOW_DOCS[symbol.tag];
11138
+ if (controlFlowDoc) return `**<${symbol.tag}>** · control flow\n\n${controlFlowDoc}`;
11139
+ const component = components[symbol.tag];
11140
+ if (!component) return null;
11141
+ return describeComponent(component);
11142
+ }
11143
+ case "attribute": return describeAttribute(symbol.tag, symbol.name, components);
11144
+ case "handler": {
11145
+ const { controller } = page;
11146
+ if (controller?.mutations?.includes(symbol.name)) return `mutation \`${symbol.name}\` in controller.ts`;
11147
+ if (controller?.actions?.includes(symbol.name)) return `action \`${symbol.name}\` in controller.ts`;
11148
+ return null;
11149
+ }
11150
+ case "state": {
11151
+ if (!page.state?.keys?.includes(symbol.name)) return null;
11152
+ const shape = page.state.shapes?.[symbol.name];
11153
+ return `state \`${symbol.name}\` in state.ts${typeSuffix(shape)}`;
11154
+ }
11155
+ case "loopVariable": return `\`${symbol.variable.name}\`: ${describeLoopVariable(symbol.variable, symbol.shape)}`;
11156
+ case "property": return `\`${symbol.name}\`: \`${symbol.shape.type}\`, a property of \`${symbol.owner.type}\``;
11157
+ case "static": return `image \`${symbol.path}\``;
11158
+ }
11159
+ }
11160
+ function describeAttribute(tag, name, components) {
11161
+ if (CONTROL_FLOW_TAGS[tag]) {
11162
+ const doc = CONTROL_FLOW_ATTRIBUTE_DOCS[name];
11163
+ if (!doc) return null;
11164
+ return `\`${name}\`: ${doc}`;
11165
+ }
11166
+ const component = components[tag];
11167
+ if (!component) return null;
11168
+ const type = component.props[name];
11169
+ if (type) return `\`${name}\`: ${describeType(type)}`;
11170
+ if (name.startsWith(DATA_PREFIX)) return `\`${name}\`: this element's handlers get it as \`event.data.${dataKey(name)}\``;
11171
+ if (isEventProp(name) && component.events.includes(eventName(name))) return `\`${name}\`: on \`${eventName(name)}\`, runs a mutation or action from controller.ts`;
11172
+ return null;
11173
+ }
11174
+ function locationIn(file, text, offset) {
11175
+ if (offset === void 0) return null;
11176
+ const position = TextDocument.create("", "typescript", 0, text).positionAt(offset);
11177
+ return {
11178
+ uri: pathToFileURL(file).href,
11179
+ range: {
11180
+ start: position,
11181
+ end: position
11182
+ }
11183
+ };
11184
+ }
11185
+ //#endregion
11119
11186
  //#region src/lsp/server.ts
11120
- const WATCHED = [
11187
+ const WATCHED_FILES = [
11121
11188
  "**/*.ts",
11122
11189
  "**/dot.json",
11123
11190
  "**/statics/**"
11124
11191
  ];
11125
11192
  function startLanguageServer(connection) {
11126
11193
  const documents = new import_main.TextDocuments(TextDocument);
11127
- const features = { components: builtinRegistry };
11194
+ const components = builtinRegistry;
11128
11195
  const stateTypes = new StateTypes();
11129
11196
  let canWatchFiles = false;
11130
11197
  const readText = async (file) => {
11131
- const open = documents.get(pathToFileURL(file).href);
11132
- if (open) return open.getText();
11133
- return existsSync(file) ? readFile(file, "utf8") : void 0;
11198
+ const openDocument = documents.get(pathToFileURL(file).href);
11199
+ if (openDocument) return openDocument.getText();
11200
+ if (!existsSync(file)) return;
11201
+ return readFile(file, "utf8");
11134
11202
  };
11135
- const pageOf = async (uri) => {
11203
+ const openView = async (uri) => {
11136
11204
  const document = documents.get(uri);
11137
11205
  if (!document || !uri.endsWith(".dml")) return;
11138
11206
  const file = fileURLToPath(uri);
11139
11207
  return {
11140
11208
  document,
11141
- page: await readPage(file, readText, stateTypes),
11142
- file
11209
+ file,
11210
+ page: await readPage(file, readText, stateTypes)
11143
11211
  };
11144
11212
  };
11145
11213
  const validate = async (uri) => {
11146
11214
  try {
11147
- const found = await pageOf(uri);
11148
- if (!found) return;
11149
- const diagnostics = diagnose(found.document, found.file, found.page, features);
11215
+ const view = await openView(uri);
11216
+ if (!view) return;
11217
+ const diagnostics = diagnose(view.document, view.file, view.page, components);
11150
11218
  await connection.sendDiagnostics({
11151
11219
  uri,
11152
- version: found.document.version,
11220
+ version: view.document.version,
11153
11221
  diagnostics
11154
11222
  });
11155
11223
  } catch (error) {
@@ -11178,10 +11246,14 @@ function startLanguageServer(connection) {
11178
11246
  };
11179
11247
  });
11180
11248
  connection.onInitialized(() => {
11181
- if (canWatchFiles) connection.client.register(import_main.DidChangeWatchedFilesNotification.type, { watchers: WATCHED.map((globPattern) => ({ globPattern })) });
11249
+ if (canWatchFiles) {
11250
+ const watchers = WATCHED_FILES.map((globPattern) => ({ globPattern }));
11251
+ connection.client.register(import_main.DidChangeWatchedFilesNotification.type, { watchers });
11252
+ }
11182
11253
  });
11183
11254
  documents.onDidChangeContent(({ document }) => {
11184
- document.uri.endsWith(".dml") ? validate(document.uri) : validateAll();
11255
+ if (document.uri.endsWith(".dml")) validate(document.uri);
11256
+ else validateAll();
11185
11257
  });
11186
11258
  documents.onDidClose(({ document }) => {
11187
11259
  connection.sendDiagnostics({
@@ -11193,19 +11265,22 @@ function startLanguageServer(connection) {
11193
11265
  validateAll();
11194
11266
  });
11195
11267
  connection.onCompletion(async ({ textDocument, position }) => {
11196
- const found = await pageOf(textDocument.uri);
11197
- if (!found) return [];
11198
- return complete(found.document, found.document.offsetAt(position), found.page, features);
11268
+ const view = await openView(textDocument.uri);
11269
+ if (!view) return [];
11270
+ const offset = view.document.offsetAt(position);
11271
+ return complete(view.document, offset, view.page, components);
11199
11272
  });
11200
11273
  connection.onHover(async ({ textDocument, position }) => {
11201
- const found = await pageOf(textDocument.uri);
11202
- if (!found) return null;
11203
- return hover(found.document, found.document.offsetAt(position), found.page, features);
11274
+ const view = await openView(textDocument.uri);
11275
+ if (!view) return null;
11276
+ const offset = view.document.offsetAt(position);
11277
+ return hover(view.document, offset, view.page, components);
11204
11278
  });
11205
11279
  connection.onDefinition(async ({ textDocument, position }) => {
11206
- const found = await pageOf(textDocument.uri);
11207
- if (!found) return null;
11208
- return definition(found.document, found.document.offsetAt(position), found.page);
11280
+ const view = await openView(textDocument.uri);
11281
+ if (!view) return null;
11282
+ const offset = view.document.offsetAt(position);
11283
+ return definition(view.document, offset, view.page);
11209
11284
  });
11210
11285
  documents.listen(connection);
11211
11286
  connection.listen();
@@ -11217,4 +11292,4 @@ function serveStdio() {
11217
11292
  //#endregion
11218
11293
  export { serveStdio };
11219
11294
 
11220
- //# sourceMappingURL=server-uIyYmYnp.js.map
11295
+ //# sourceMappingURL=server-B9-F93mS.js.map