@lucidkiln/language-server 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -411,7 +411,7 @@ const splitParameters = (list) => {
411
411
  const DOCS = {
412
412
  tags: {
413
413
  "add": {
414
- "markdown": "- Content goes to the outlet with the same name, in the order it was added. It doesn't matter whether `add to` runs before or after the outlet.\n- The content lines up with the outlet's indentation.\n- The content can read the variables around it. Variables you `set` inside it stay inside it.\n- Content for an outlet the page doesn't have is dropped. A component still works on a page without that outlet, and when you [render one block](https://kilnlang.com/docs/api/rendering/#rendering-one-block).",
414
+ "markdown": "- Content goes to the outlet with the same name, in the order it was added. It doesn't matter whether `add to` runs before or after the outlet.\n- The content can read the variables around it. Variables you `set` inside it stay inside it.\n- Content for an outlet the page doesn't have is dropped. A component still works on a page without that outlet, and when you [render one block](https://kilnlang.com/docs/api/rendering/#rendering-one-block).",
415
415
  "url": "https://kilnlang.com/docs/tags/outlet/#adding-content"
416
416
  },
417
417
  "apply": {
@@ -1412,19 +1412,26 @@ const bindingsAt = (scopes, offset) => {
1412
1412
  *
1413
1413
  * - `Get`: `a.b` and `a[0]`, reading a property, a map's value or a list's item.
1414
1414
  * - `Item`, `Key`: what `for` gives for each pass.
1415
- * - `Result`: what a call gives once Kiln waits for it. `Params` and `Returns` describe the function for hover.
1415
+ * - `Params` and `Returns` describe a function for hover. A function made with `withRender` is an object whose `call`
1416
+ * takes `RenderInfo` first, so `Params` leaves that out.
1417
+ * - `arg`, `plain`, `aware`, `dropRender`, `Called`: a call to a host function, written as a real call so TypeScript
1418
+ * works out a generic function's type parameters from the arguments. See `typeWriter`.
1416
1419
  * - `Members`: the values whose properties a template can read, so lists, maps and primitives complete nothing.
1417
1420
  * - `Last`: a function's last parameter, which named arguments are gathered into.
1418
1421
  * - `Fits`, `NamedFit`: whether an argument fits its parameter. Unknown types always fit, so the editor never guesses.
1419
1422
  */
1420
- const PRELUDE = `import type { KilnFilters, KilnFunctions, KilnGlobals, KilnTests, KilnTypes, Markup, RenderAware } from "@lucidkiln/core";
1423
+ const PRELUDE = `import type { KilnFilters, KilnFunctions, KilnGlobals, KilnTests, KilnTypes, Markup } from "@lucidkiln/core";
1421
1424
  type Named<N extends string> = N extends keyof KilnTypes ? KilnTypes[N] : unknown;
1422
1425
  type Get<V, K> = NonNullable<V> extends infer O ? O extends ReadonlyMap<unknown, infer M> ? M : O extends readonly (infer I)[] ? (K extends number ? I : unknown) : K extends keyof O ? O[K] : unknown : never;
1423
1426
  type Item<V> = NonNullable<V> extends infer O ? O extends string ? unknown : O extends readonly (infer I)[] ? I : O extends ReadonlyMap<unknown, infer M> ? M : O extends ReadonlySet<infer S> ? S : O extends Iterable<infer I> ? I : O extends object ? O[keyof O] : unknown : never;
1424
1427
  type Key<V> = NonNullable<V> extends infer O ? O extends string ? unknown : O extends readonly unknown[] | ReadonlySet<unknown> ? number : O extends ReadonlyMap<infer K, unknown> ? K : O extends object ? keyof O & string : unknown : never;
1425
- type Returns<F> = F extends RenderAware<any, any, infer R> ? R : F extends (...args: any) => infer R ? R : unknown;
1426
- type Result<F> = Awaited<Returns<F>>;
1427
- type Params<F> = F extends RenderAware<any, infer A, any> ? A : F extends (...args: infer A) => any ? A : never;
1428
+ type Returns<F> = F extends (...args: never) => infer R ? R : F extends { readonly call: (...args: never) => infer R } ? R : unknown;
1429
+ type Params<F> = F extends (...args: infer A) => unknown ? A : F extends { readonly call: (render: never, ...args: infer A) => unknown } ? A : never;
1430
+ declare function arg<T>(): T;
1431
+ declare function plain<F>(fn: F): F extends (...args: never) => unknown ? F : never;
1432
+ declare function aware<F>(fn: F): F extends (...args: never) => unknown ? never : F extends { readonly call: infer C } ? C : never;
1433
+ declare function dropRender<A extends unknown[], R>(fn: (render: never, ...args: A) => R): (...args: A) => R;
1434
+ type Called<F, Plain, Aware> = unknown extends F ? unknown : F extends (...args: never) => unknown ? Awaited<Plain> : F extends { readonly call: unknown } ? Awaited<Aware> : unknown;
1428
1435
  type Tail<A> = A extends readonly [unknown?, ...infer R] ? R : A;
1429
1436
  type Last<A> = Required<A> extends readonly [...unknown[], infer L] ? L : A extends readonly (infer E)[] ? E : unknown;
1430
1437
  type Fits<A, P> = [P] extends [never] ? true : unknown extends P ? true : unknown extends A ? true : [Exclude<A, Markup>] extends [P] ? true : false;
@@ -1513,88 +1520,135 @@ const ITEM_PARAMS = {
1513
1520
  reduce: [1]
1514
1521
  };
1515
1522
  const MAX_DEPTH = 32;
1516
- /** The TypeScript type of a variable, from where its value comes from. */
1517
- const bindingType = (source, context, depth = 0) => {
1518
- const of = (expression) => expressionType(expression, context, depth + 1);
1519
- switch (source.kind) {
1520
- case "prop": return propType(source.type);
1521
- case "rest": return "Readonly<Record<string, unknown>>";
1522
- case "set": return of(source.value);
1523
- case "capture": return "Markup";
1524
- case "item": return `Item<${of(source.iterable)}>`;
1525
- case "key": return `Key<${of(source.iterable)}>`;
1526
- case "arrow": return ITEM_PARAMS[source.filter]?.includes(source.index) ? `Item<${of(source.input)}>` : "unknown";
1527
- case "with": return `Get<${of(source.value)}, ${JSON.stringify(source.key)}>`;
1528
- default: return "unknown";
1529
- }
1530
- };
1531
1523
  const union = (types) => types.length === 0 ? "never" : [...new Set(types)].join(" | ");
1532
1524
  const literalKey = (expression) => expression.type === "Literal" && (typeof expression.value === "string" || typeof expression.value === "number") ? JSON.stringify(expression.value) : null;
1533
- /** Writes the TypeScript type of a Kiln expression, using the helper types in `PRELUDE`. */
1534
- const expressionType = (expression, context, depth = 0) => {
1535
- if (depth > MAX_DEPTH) return "unknown";
1536
- const of = (inner) => expressionType(inner, context, depth + 1);
1537
- const args = (list) => list.map((arg) => arg.type === "Spread" ? "unknown" : of(arg.value));
1538
- switch (expression.type) {
1539
- case "Literal": return expression.value === null ? "null" : typeof expression.value;
1540
- case "Interpolation": return "string";
1541
- case "Array": return `(${union(expression.elements.map((element) => element.type === "Spread" ? `Item<${of(element.value)}>` : of(element)))})[]`;
1542
- case "Hash": return `{ ${expression.entries.flatMap((entry) => {
1543
- if (entry.type === "Spread") return [];
1544
- const key = entry.key.type === "Name" ? JSON.stringify(entry.key.name) : literalKey(entry.key);
1545
- return key ? [`${key}: ${of(entry.value)}`] : [];
1546
- }).join("; ")} }`;
1547
- case "Unary": return expression.operator === "not" ? "boolean" : "number";
1548
- case "Binary": switch (expression.operator) {
1549
- case "??":
1550
- case "?:": return `NonNullable<${of(expression.left)}> | ${of(expression.right)}`;
1551
- case "~": return "string";
1552
- case "..": return "number[]";
1553
- case "+":
1554
- case "-":
1555
- case "*":
1556
- case "/":
1557
- case "//":
1558
- case "%":
1559
- case "**": return "number";
1560
- default: return "boolean";
1561
- }
1562
- case "Conditional": return union([of(expression.consequent), expression.alternate ? of(expression.alternate) : "null"]);
1563
- case "Member": {
1564
- const key = literalKey(expression.property) ?? "string";
1565
- return `Get<${of(expression.object)}, ${key}>`;
1525
+ /**
1526
+ * Writes the types of a template's values. A literal's type is its value, so `"page"` is `"page"`. A call to a host
1527
+ * function or filter becomes a real call in `statements`, so TypeScript works out a generic function's type parameters
1528
+ * from the arguments:
1529
+ *
1530
+ * ```ts
1531
+ * declare const $fn0: Get<KilnFunctions, "document">;
1532
+ * const $plain0 = plain($fn0)(arg<"page">());
1533
+ * const $aware0 = dropRender(aware($fn0))(arg<"page">());
1534
+ * ```
1535
+ *
1536
+ * The call's type is `Called<typeof $fn0, typeof $plain0, typeof $aware0>`: the first call for a plain function, the
1537
+ * second for one made with `withRender`. Each expression is written once, however often a template reads it.
1538
+ */
1539
+ const typeWriter = (context) => {
1540
+ const statements = [];
1541
+ const written = /* @__PURE__ */ new Map();
1542
+ /** Writes a call to `callee`, a TypeScript type, with the types of its arguments, and returns the result's type. */
1543
+ const call = (callee, args) => {
1544
+ const id = statements.length;
1545
+ const list = args.join(", ");
1546
+ statements.push(`declare const $fn${id}: ${callee};\nconst $plain${id} = plain($fn${id})(${list});\nconst $aware${id} = dropRender(aware($fn${id}))(${list});\n`);
1547
+ return `Called<typeof $fn${id}, typeof $plain${id}, typeof $aware${id}>`;
1548
+ };
1549
+ /** A call's arguments as TypeScript: named arguments are gathered into one hash at the end, as Kiln does. */
1550
+ const callArgs = (list, depth, input) => {
1551
+ const named = list.flatMap((item) => item.type === "Named" ? [`${JSON.stringify(item.name)}: ${expression(item.value, depth)}`] : []);
1552
+ return [
1553
+ ...input ? [`arg<${input}>()`] : [],
1554
+ ...list.flatMap((item) => {
1555
+ if (item.type === "Named") return [];
1556
+ const type = expression(item.value, depth);
1557
+ return [item.type === "Spread" ? `...arg<Item<${type}>[]>()` : `arg<${type}>()`];
1558
+ }),
1559
+ ...named.length > 0 ? [`arg<{ ${named.join("; ")} }>()`] : []
1560
+ ];
1561
+ };
1562
+ const binding = (source, depth) => {
1563
+ const of = (value) => expression(value, depth + 1);
1564
+ switch (source.kind) {
1565
+ case "prop": return propType(source.type);
1566
+ case "rest": return "Readonly<Record<string, unknown>>";
1567
+ case "set": return of(source.value);
1568
+ case "capture": return "Markup";
1569
+ case "item": return `Item<${of(source.iterable)}>`;
1570
+ case "key": return `Key<${of(source.iterable)}>`;
1571
+ case "arrow": return ITEM_PARAMS[source.filter]?.includes(source.index) ? `Item<${of(source.input)}>` : "unknown";
1572
+ case "with": return `Get<${of(source.value)}, ${JSON.stringify(source.key)}>`;
1573
+ default: return "unknown";
1566
1574
  }
1567
- case "Call": {
1568
- const { callee } = expression;
1569
- if (callee.type === "Member") {
1570
- const key = literalKey(callee.property) ?? "string";
1571
- return `Result<Get<${of(callee.object)}, ${key}>>`;
1575
+ };
1576
+ const expression = (node, depth) => {
1577
+ if (depth > MAX_DEPTH) return "unknown";
1578
+ const known = written.get(node);
1579
+ if (known) return known;
1580
+ const type = write(node, depth + 1);
1581
+ written.set(node, type);
1582
+ return type;
1583
+ };
1584
+ const write = (node, depth) => {
1585
+ const of = (inner) => expression(inner, depth);
1586
+ switch (node.type) {
1587
+ case "Literal": return typeof node.value === "string" ? JSON.stringify(node.value) : String(node.value);
1588
+ case "Interpolation": return "string";
1589
+ case "Array": return `(${union(node.elements.map((element) => element.type === "Spread" ? `Item<${of(element.value)}>` : of(element)))})[]`;
1590
+ case "Hash": return `{ ${node.entries.flatMap((entry) => {
1591
+ if (entry.type === "Spread") return [];
1592
+ const key = entry.key.type === "Name" ? JSON.stringify(entry.key.name) : literalKey(entry.key);
1593
+ return key ? [`${key}: ${of(entry.value)}`] : [];
1594
+ }).join("; ")} }`;
1595
+ case "Unary": return node.operator === "not" ? "boolean" : "number";
1596
+ case "Binary": switch (node.operator) {
1597
+ case "??":
1598
+ case "?:": return `NonNullable<${of(node.left)}> | ${of(node.right)}`;
1599
+ case "~": return "string";
1600
+ case "..": return "number[]";
1601
+ case "+":
1602
+ case "-":
1603
+ case "*":
1604
+ case "/":
1605
+ case "//":
1606
+ case "%":
1607
+ case "**": return "number";
1608
+ default: return "boolean";
1572
1609
  }
1573
- if (callee.type !== "Name") return "unknown";
1574
- const name = callee.name;
1575
- if (context.macros.has(name)) return "Markup";
1576
- const builtin = BUILTIN_FUNCTIONS[name];
1577
- if (builtin && !context.functions.includes(name)) return builtin;
1578
- return `Result<Get<KilnFunctions, ${JSON.stringify(name)}>>`;
1579
- }
1580
- case "Filter": {
1581
- const input = of(expression.input);
1582
- const builtin = BUILTIN_FILTERS[expression.name];
1583
- if (!builtin || context.filters.includes(expression.name)) return `Result<Get<KilnFilters, ${JSON.stringify(expression.name)}>>`;
1584
- const arrow = expression.args[0]?.value;
1585
- if (expression.name === "map" && arrow?.type === "Arrow") return `(${of(arrow.body)})[]`;
1586
- return builtin(input, args(expression.args));
1587
- }
1588
- case "Test": return "boolean";
1589
- case "OptionalChain": return of(expression.expression);
1590
- case "Name": {
1591
- if (expression.name === "loop") return "unknown";
1592
- const binding = bindingAt(context.scopes, expression.name, expression.span.start);
1593
- if (binding) return bindingType(binding.source, context, depth + 1);
1594
- return `Get<KilnGlobals, ${JSON.stringify(expression.name)}>`;
1610
+ case "Conditional": return union([of(node.consequent), node.alternate ? of(node.alternate) : "null"]);
1611
+ case "Member": {
1612
+ const key = literalKey(node.property) ?? "string";
1613
+ return `Get<${of(node.object)}, ${key}>`;
1614
+ }
1615
+ case "Call": {
1616
+ const { callee } = node;
1617
+ if (callee.type === "Member") {
1618
+ const key = literalKey(callee.property) ?? "string";
1619
+ return call(`Get<${of(callee.object)}, ${key}>`, callArgs(node.args, depth));
1620
+ }
1621
+ if (callee.type !== "Name") return "unknown";
1622
+ const name = callee.name;
1623
+ if (context.macros.has(name)) return "Markup";
1624
+ const builtin = BUILTIN_FUNCTIONS[name];
1625
+ if (builtin && !context.functions.includes(name)) return builtin;
1626
+ return call(`Get<KilnFunctions, ${JSON.stringify(name)}>`, callArgs(node.args, depth));
1627
+ }
1628
+ case "Filter": {
1629
+ const input = of(node.input);
1630
+ const builtin = BUILTIN_FILTERS[node.name];
1631
+ if (!builtin || context.filters.includes(node.name)) return call(`Get<KilnFilters, ${JSON.stringify(node.name)}>`, callArgs(node.args, depth, input));
1632
+ const arrow = node.args[0]?.value;
1633
+ if (node.name === "map" && arrow?.type === "Arrow") return `(${of(arrow.body)})[]`;
1634
+ return builtin(input, node.args.map((item) => item.type === "Spread" ? "unknown" : of(item.value)));
1635
+ }
1636
+ case "Test": return "boolean";
1637
+ case "OptionalChain": return of(node.expression);
1638
+ case "Name": {
1639
+ if (node.name === "loop") return "unknown";
1640
+ const found = bindingAt(context.scopes, node.name, node.span.start);
1641
+ if (found) return binding(found.source, depth);
1642
+ return `Get<KilnGlobals, ${JSON.stringify(node.name)}>`;
1643
+ }
1644
+ default: return "unknown";
1595
1645
  }
1596
- default: return "unknown";
1597
- }
1646
+ };
1647
+ return {
1648
+ expression: (node) => expression(node, 0),
1649
+ binding: (source) => binding(source, 0),
1650
+ statements
1651
+ };
1598
1652
  };
1599
1653
  /** The registries a project describes in TypeScript, by the kind of name the language server looks up. */
1600
1654
  const REGISTRIES = {
@@ -1603,9 +1657,12 @@ const REGISTRIES = {
1603
1657
  test: "KilnTests",
1604
1658
  global: "KilnGlobals"
1605
1659
  };
1606
- /** Checks a file of `type name = type` aliases with the project's TypeScript, and reads it with `use`. */
1607
- const withTypes = (typescript, aliases, use) => {
1608
- const source = PRELUDE + Object.entries(aliases).map(([name, type]) => `type ${name} = ${type};\n`).join("");
1660
+ /**
1661
+ * Checks a file of `type name = type` aliases with the project's TypeScript, and reads it with `use`. `statements` are
1662
+ * what the aliases read, from a `TypeWriter`.
1663
+ */
1664
+ const withTypes = (typescript, aliases, use, statements = []) => {
1665
+ const source = PRELUDE + statements.join("") + Object.entries(aliases).map(([name, type]) => `type ${name} = ${type};\n`).join("");
1609
1666
  return typescript.query(source, use);
1610
1667
  };
1611
1668
  /** The TypeScript type of a registered function, filter or test, by kind and name. */
@@ -1639,8 +1696,11 @@ const describeCallable = (typescript, kind, name) => withTypes(typescript, {
1639
1696
  });
1640
1697
  /** The names a function or filter takes as named arguments: the properties of its last parameter. */
1641
1698
  const namedOptions = (typescript, kind, name) => withTypes(typescript, { Options: `Members<Last<${parametersType(kind, name)}>>` }, (read) => read.members("Options")) ?? [];
1642
- /** Checks arguments against their parameters with one TypeScript query. Null for an argument that fits. */
1643
- const checkArguments = (typescript, checks) => {
1699
+ /**
1700
+ * Checks arguments against their parameters with one TypeScript query. Null for an argument that fits. `statements` are
1701
+ * what the argument types read, from a `TypeWriter`.
1702
+ */
1703
+ const checkArguments = (typescript, checks, statements = []) => {
1644
1704
  if (checks.length === 0) return [];
1645
1705
  const aliases = {};
1646
1706
  checks.forEach((check, index) => {
@@ -1658,7 +1718,7 @@ const checkArguments = (typescript, checks) => {
1658
1718
  expected: read.describe(`Expected${index}`) ?? "unknown",
1659
1719
  actual: read.describe(`Actual${index}`) ?? "unknown"
1660
1720
  };
1661
- })) ?? checks.map(() => null);
1721
+ }), statements) ?? checks.map(() => null);
1662
1722
  };
1663
1723
  //#endregion
1664
1724
  //#region src/features.ts
@@ -1703,7 +1763,7 @@ const builtinItems = (builtins, docKind, kind, extra) => [...Object.entries(buil
1703
1763
  kind,
1704
1764
  detail: "From your project"
1705
1765
  }))];
1706
- /** Parses a template and finds its variables, with what `expressionType` needs to work out their types. */
1766
+ /** Parses a template and finds its variables, with what `typeWriter` needs to work out their types. */
1707
1767
  const templateTypes = (open, text) => {
1708
1768
  const manifest = open.project.manifest;
1709
1769
  const scopes = templateScopes(text, (name) => name in TESTS || (manifest?.tests.includes(name) ?? false));
@@ -1739,8 +1799,9 @@ const memberDocs = (member) => [member.docs, ...member.tags.map((tag) => `*@${ta
1739
1799
  const membersOf = async (open, object, context) => {
1740
1800
  const typescript = await open.project.typescript();
1741
1801
  if (!typescript) return [];
1742
- const type = expressionType(object, context);
1743
- return withTypes(typescript, { Value: `Members<${type}>` }, (read) => read.members("Value")) ?? [];
1802
+ const types = typeWriter(context);
1803
+ const type = types.expression(object);
1804
+ return withTypes(typescript, { Value: `Members<${type}>` }, (read) => read.members("Value"), types.statements) ?? [];
1744
1805
  };
1745
1806
  /** A function, filter, test or global the project describes in TypeScript. */
1746
1807
  const registered = async (open, kind, name) => {
@@ -1962,7 +2023,8 @@ const hover = async (open, offset) => {
1962
2023
  value = sections(codeBlock(prop ? describeProp(prop) : `...${symbol.name}`), BINDING_NOTES[binding.source.kind]);
1963
2024
  } else if (binding) {
1964
2025
  const typescript = await project.typescript();
1965
- const type = typescript ? withTypes(typescript, { Value: bindingType(binding.source, types.context) }, (read) => read.describe("Value")) : null;
2026
+ const writer = typeWriter(types.context);
2027
+ const type = typescript ? withTypes(typescript, { Value: writer.binding(binding.source) }, (read) => read.describe("Value"), writer.statements) : null;
1966
2028
  value = sections(codeBlock(type && type !== "unknown" ? `${symbol.name}: ${type}` : symbol.name, "ts"), BINDING_NOTES[binding.source.kind]);
1967
2029
  } else {
1968
2030
  const global = await registered(open, "global", symbol.name);
@@ -2263,6 +2325,7 @@ const argumentWarnings = async (open) => {
2263
2325
  if (!typescript) return [];
2264
2326
  const text = open.document.getText();
2265
2327
  const { scopes, context } = templateTypes(open, text);
2328
+ const types = typeWriter(context);
2266
2329
  /**
2267
2330
  * Each check, with what it's for: a filter's input, a positional or a named argument. A wrong type is reported on
2268
2331
  * the value, and a named argument the function doesn't take on its name.
@@ -2284,7 +2347,7 @@ const argumentWarnings = async (open) => {
2284
2347
  const parameters = parametersType(kind, name, true);
2285
2348
  [...node.type === "Filter" ? [node.input] : [], ...node.args.flatMap((arg) => arg.type === "Positional" ? [arg.value] : [])].forEach((value, index) => {
2286
2349
  checks.push({
2287
- type: expressionType(value, context),
2350
+ type: types.expression(value),
2288
2351
  parameters,
2289
2352
  index,
2290
2353
  span: value.span,
@@ -2295,7 +2358,7 @@ const argumentWarnings = async (open) => {
2295
2358
  for (const arg of node.args) {
2296
2359
  if (arg.type !== "Named") continue;
2297
2360
  checks.push({
2298
- type: expressionType(arg.value, context),
2361
+ type: types.expression(arg.value),
2299
2362
  parameters,
2300
2363
  name: arg.name,
2301
2364
  span: arg.value.span,
@@ -2308,7 +2371,7 @@ const argumentWarnings = async (open) => {
2308
2371
  });
2309
2372
  }
2310
2373
  }
2311
- return checkArguments(typescript, checks).flatMap((problem, index) => {
2374
+ return checkArguments(typescript, checks, types.statements).flatMap((problem, index) => {
2312
2375
  const check = checks[index];
2313
2376
  if (!problem || !check) return [];
2314
2377
  if (problem.kind === "missing") {