diff --git a/lsp/typescript-expression-plugin/src/__tests__/service.test.ts b/lsp/typescript-expression-plugin/src/__tests__/service.test.ts index 28fb511..541e723 100644 --- a/lsp/typescript-expression-plugin/src/__tests__/service.test.ts +++ b/lsp/typescript-expression-plugin/src/__tests__/service.test.ts @@ -1,6 +1,6 @@ import { test, expect, describe, beforeEach } from "vitest"; import { CommonExpressions } from "@player-lang/static-xlrs"; -import { symbolDisplayToString } from "@xlr-lib/xlr-utils"; +import { symbolDisplayToString } from "../utils"; import { ExpressionLanguageService } from "../service"; describe("language-service", () => { diff --git a/lsp/typescript-expression-plugin/src/service.ts b/lsp/typescript-expression-plugin/src/service.ts index 16943ab..d3cdb31 100644 --- a/lsp/typescript-expression-plugin/src/service.ts +++ b/lsp/typescript-expression-plugin/src/service.ts @@ -5,7 +5,7 @@ import type { Logger, } from "typescript-template-language-service-decorator"; import type { FunctionType, TSManifest, NodeType } from "@xlr-lib/xlr"; -import { createTSDocString } from "@xlr-lib/xlr-utils"; +import { createTSDocString } from "./utils"; import { XLRSDK } from "@xlr-lib/xlr-sdk"; import type { ExpressionNode } from "@player-ui/player"; import { parseExpression } from "@player-ui/player"; diff --git a/lsp/typescript-expression-plugin/src/utils.ts b/lsp/typescript-expression-plugin/src/utils.ts index 93d9abf..02bc7a5 100644 --- a/lsp/typescript-expression-plugin/src/utils.ts +++ b/lsp/typescript-expression-plugin/src/utils.ts @@ -1,9 +1,241 @@ -import type { TextSpan } from "typescript"; +import ts from "typescript/lib/tsserverlibrary"; +import type { TextSpan, SymbolDisplayPart } from "typescript"; import type { Position } from "vscode-languageserver-types"; import type { ExpressionNode, NodeLocation } from "@player-ui/player"; +import type { NodeType } from "@xlr-lib/xlr"; +import { isPrimitiveTypeNode } from "@xlr-lib/xlr-utils"; import { parseTree } from "jsonc-parser"; import type { Node } from "jsonc-parser"; +const { SymbolDisplayPartKind } = ts; + +/** Like `.join()` but for arrays */ +function insertBetweenElements(array: Array, separator: T): T[] { + return array.reduce((acc, item, index) => { + if (index === 0) { + return [item]; + } + + return [...acc, separator, item]; + }, [] as T[]); +} + +/** Convert the TS SymbolDisplayParts into a single string */ +export function symbolDisplayToString( + displayParts: Array, +): string { + return ts.displayPartsToString(displayParts); +} + +/** Generate a documentation string for a given XLR node */ +export function createTSDocString(node: NodeType): Array { + if (node.type === "ref") { + return [ + { + text: node.ref, + kind: SymbolDisplayPartKind.keyword as any, + }, + ]; + } + + if (node.type === "or" || node.type === "and") { + const items = node.type === "and" ? node.and : node.or; + + return insertBetweenElements( + items.map((subnode) => createTSDocString(subnode)), + [ + { + kind: SymbolDisplayPartKind.punctuation as any, + text: node.type === "and" ? " & " : " | ", + }, + ], + ).flat(); + } + + if (node.type === "function") { + return [ + { + kind: SymbolDisplayPartKind.keyword as any, + text: "function", + }, + { + kind: SymbolDisplayPartKind.space as any, + text: " ", + }, + ...(node.name + ? [{ text: node.name, kind: SymbolDisplayPartKind.methodName }] + : []), + { + kind: SymbolDisplayPartKind.punctuation as any, + text: "(", + }, + ...insertBetweenElements( + node.parameters.map((p) => { + if (p.name) { + return [ + { + kind: SymbolDisplayPartKind.parameterName as any, + text: p.name, + }, + { + kind: SymbolDisplayPartKind.punctuation as any, + text: p.optional ? "?" : "", + }, + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ": ", + }, + ...createTSDocString(p.type), + ]; + } + + return createTSDocString(p.type); + }), + [ + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ", ", + }, + ], + ).flat(), + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ")", + }, + ...(node.returnType + ? [ + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ": ", + }, + ...createTSDocString(node.returnType), + ] + : []), + ]; + } + + if (node.type === "tuple") { + return [ + { + kind: SymbolDisplayPartKind.punctuation as any, + text: "[", + }, + ...insertBetweenElements( + node.elementTypes.map((t) => { + if (t.name) { + return [ + { + kind: SymbolDisplayPartKind.propertyName as any, + text: t.name, + }, + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ": ", + }, + ...createTSDocString(t.type), + ]; + } + + return createTSDocString(t.type); + }), + [ + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ", ", + }, + ], + ).flat(), + { + kind: SymbolDisplayPartKind.punctuation as any, + text: "]", + }, + ]; + } + + if (node.type === "array") { + return [ + { + kind: SymbolDisplayPartKind.interfaceName as any, + text: "Array", + }, + { + kind: SymbolDisplayPartKind.punctuation as any, + text: "<", + }, + ...createTSDocString(node.elementType), + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ">", + }, + ]; + } + + if (node.type === "record") { + return [ + { + kind: SymbolDisplayPartKind.interfaceName as any, + text: "Record", + }, + { + kind: SymbolDisplayPartKind.punctuation as any, + text: "<", + }, + ...createTSDocString(node.keyType), + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ", ", + }, + ...createTSDocString(node.valueType), + { + kind: SymbolDisplayPartKind.punctuation as any, + text: ">", + }, + ]; + } + + if ( + (node.type === "string" || + node.type === "boolean" || + node.type === "number") && + node.const !== undefined + ) { + return [ + { + kind: SymbolDisplayPartKind.keyword as any, + text: + typeof node.const === "string" + ? `"${node.const}"` + : String(node.const), + }, + ]; + } + + if (isPrimitiveTypeNode(node) && node.type !== "null") { + return [ + { + kind: SymbolDisplayPartKind.keyword as any, + text: node.type, + }, + ]; + } + + if (node.type === "object" && node.name) { + return [ + { + kind: SymbolDisplayPartKind.interfaceName as any, + text: node.name, + }, + ]; + } + + return [ + { + kind: SymbolDisplayPartKind.localName as any, + text: node.type, + }, + ]; +} + /** Check if the vscode position overlaps with the expression location */ export function isInRange(position: Position, location: NodeLocation) { return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be44b35..d4c8473 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,10 @@ importers: .: dependencies: '@player-ui/player': - specifier: ^0.15.0 + specifier: '*' version: 0.15.0 '@player-ui/types': - specifier: ^0.15.0 + specifier: '*' version: 0.15.0 '@types/node': specifier: ^18.18.0 @@ -28,17 +28,17 @@ importers: specifier: ^1.4.0 version: 1.6.2(typescript@5.5.4) '@xlr-lib/xlr': - specifier: 0.1.1-next.4 - version: 0.1.1-next.4 + specifier: ^1.0.0 + version: 1.0.0 '@xlr-lib/xlr-converters': - specifier: 0.1.1-next.4 - version: 0.1.1-next.4(jsonc-parser@2.3.1)(typescript@5.5.4) + specifier: ^1.0.0 + version: 1.0.0(jsonc-parser@2.3.1) '@xlr-lib/xlr-sdk': - specifier: 0.1.1-next.4 - version: 0.1.1-next.4(typescript@5.5.4) + specifier: ^1.0.0 + version: 1.0.0 '@xlr-lib/xlr-utils': - specifier: 0.1.1-next.4 - version: 0.1.1-next.4(jsonc-parser@2.3.1)(typescript@5.5.4) + specifier: ^1.0.0 + version: 1.0.0(jsonc-parser@2.3.1) dequal: specifier: ^2.0.2 version: 2.0.3 @@ -1011,25 +1011,20 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==, tarball: https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz} - '@xlr-lib/xlr-converters@0.1.1-next.4': - resolution: {integrity: sha512-D5Sag7X2dQbya/l4DBqr/lyUDmu7ADDtKQotRdA8KixZE7JMJoI9CW5itera0PAo3WvIA7lTPTN1CrEvlTCe1A==, tarball: https://registry.npmjs.org/@xlr-lib/xlr-converters/-/xlr-converters-0.1.1-next.4.tgz} + '@xlr-lib/xlr-converters@1.0.0': + resolution: {integrity: sha512-TovXuu4VMwJBWZnAhhO93jncx5NXfn1khy1mYVIgMXrC7b1c++g6EyxLs4svkUAiQiynEkmSa96QvydSgAeYzw==, tarball: https://registry.npmjs.org/@xlr-lib/xlr-converters/-/xlr-converters-1.0.0.tgz} hasBin: true - peerDependencies: - typescript: 5.5.4 - '@xlr-lib/xlr-sdk@0.1.1-next.4': - resolution: {integrity: sha512-q46qn+7yzMJHT09+hHj0hej2t/p+IpIBT48BptlsKpEugENpFihrK4ITGJA6TO7QgtG58AKZsgTx95z8ebisyg==, tarball: https://registry.npmjs.org/@xlr-lib/xlr-sdk/-/xlr-sdk-0.1.1-next.4.tgz} - peerDependencies: - typescript: 5.5.4 + '@xlr-lib/xlr-sdk@1.0.0': + resolution: {integrity: sha512-3ge1NTzCukn9F76HQPZXeNZI/C/HOJF365II6m5GbzhR1Aag9Ghf4/5w5/PIrfb734eZ5kFkMiF6UeMOXUegtQ==, tarball: https://registry.npmjs.org/@xlr-lib/xlr-sdk/-/xlr-sdk-1.0.0.tgz} - '@xlr-lib/xlr-utils@0.1.1-next.4': - resolution: {integrity: sha512-BDCQnfHy1SVY0bB6BByshpsSihgJ7imqNV9uhuk2OD003uhawOFa5WH4HRjmWnvXXuWp/5asjB9ObIwkNh3m4g==, tarball: https://registry.npmjs.org/@xlr-lib/xlr-utils/-/xlr-utils-0.1.1-next.4.tgz} + '@xlr-lib/xlr-utils@1.0.0': + resolution: {integrity: sha512-swoRx3QppioaClwvf+89yJrPJ8oEsIJNWUdney7Vom9XmUKbVY4XgPF0fZSYSC+HWsvB8NbkQ5CNCE8AXW6oxQ==, tarball: https://registry.npmjs.org/@xlr-lib/xlr-utils/-/xlr-utils-1.0.0.tgz} peerDependencies: jsonc-parser: 2.3.1 - typescript: 5.5.4 - '@xlr-lib/xlr@0.1.1-next.4': - resolution: {integrity: sha512-FCi+Fc0KQHUbKO+OTUp291aFcC/t7pmjtMYM6lQdXB55Lu+EV47jE3PjHqxZuS3LSitGxonXMEOrprQDrqL9MQ==, tarball: https://registry.npmjs.org/@xlr-lib/xlr/-/xlr-0.1.1-next.4.tgz} + '@xlr-lib/xlr@1.0.0': + resolution: {integrity: sha512-Zfu6/lLauRQafuUo+JvbKZ2lmQkxtDgJxnGtWieGNct6mSHhWKWRc7siXQlboBxwu84yMcjLSrOxvD9WrBtLWQ==, tarball: https://registry.npmjs.org/@xlr-lib/xlr/-/xlr-1.0.0.tgz} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, tarball: https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz} @@ -3908,35 +3903,32 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 - '@xlr-lib/xlr-converters@0.1.1-next.4(jsonc-parser@2.3.1)(typescript@5.5.4)': + '@xlr-lib/xlr-converters@1.0.0(jsonc-parser@2.3.1)': dependencies: - '@xlr-lib/xlr': 0.1.1-next.4 - '@xlr-lib/xlr-utils': 0.1.1-next.4(jsonc-parser@2.3.1)(typescript@5.5.4) + '@xlr-lib/xlr': 1.0.0 + '@xlr-lib/xlr-utils': 1.0.0(jsonc-parser@2.3.1) tslib: 2.8.1 typescript: 5.5.4 transitivePeerDependencies: - jsonc-parser - '@xlr-lib/xlr-sdk@0.1.1-next.4(typescript@5.5.4)': + '@xlr-lib/xlr-sdk@1.0.0': dependencies: '@types/fs-extra': 9.0.13 '@types/node': 18.19.130 - '@xlr-lib/xlr': 0.1.1-next.4 - '@xlr-lib/xlr-converters': 0.1.1-next.4(jsonc-parser@2.3.1)(typescript@5.5.4) - '@xlr-lib/xlr-utils': 0.1.1-next.4(jsonc-parser@2.3.1)(typescript@5.5.4) + '@xlr-lib/xlr': 1.0.0 + '@xlr-lib/xlr-utils': 1.0.0(jsonc-parser@2.3.1) fs-extra: 10.1.0 jsonc-parser: 2.3.1 tslib: 2.8.1 - typescript: 5.5.4 - '@xlr-lib/xlr-utils@0.1.1-next.4(jsonc-parser@2.3.1)(typescript@5.5.4)': + '@xlr-lib/xlr-utils@1.0.0(jsonc-parser@2.3.1)': dependencies: - '@xlr-lib/xlr': 0.1.1-next.4 + '@xlr-lib/xlr': 1.0.0 jsonc-parser: 2.3.1 tslib: 2.8.1 - typescript: 5.5.4 - '@xlr-lib/xlr@0.1.1-next.4': + '@xlr-lib/xlr@1.0.0': dependencies: tslib: 2.8.1