From 60401c0225554245e4036f15970b157f2a56f15a Mon Sep 17 00:00:00 2001 From: pcardune Date: Mon, 25 Oct 2021 15:23:49 -0700 Subject: [PATCH] Remove subclassing of ASTNode --- .../codemirror-blocks/spec/activation-test.ts | 18 +- packages/codemirror-blocks/spec/ast-test.ts | 141 +-- packages/codemirror-blocks/spec/drag-test.ts | 6 +- packages/codemirror-blocks/spec/focus-test.ts | 10 +- .../codemirror-blocks/spec/new-blocks-test.ts | 10 +- .../spec/performEdits-test.ts | 5 +- .../spec/ui/Toolbar-test.tsx | 6 +- .../src/CodeMirrorBlocks.tsx | 4 +- packages/codemirror-blocks/src/ast.ts | 3 +- .../src/languages/example/ExampleParser.ts | 6 +- .../src/languages/wescheme/WeschemeParser.js | 2 +- packages/codemirror-blocks/src/nodes.tsx | 1100 ++++++++--------- 12 files changed, 636 insertions(+), 675 deletions(-) diff --git a/packages/codemirror-blocks/spec/activation-test.ts b/packages/codemirror-blocks/spec/activation-test.ts index 1cbdc4e87..240296e53 100644 --- a/packages/codemirror-blocks/spec/activation-test.ts +++ b/packages/codemirror-blocks/spec/activation-test.ts @@ -12,7 +12,7 @@ import { } from "../src/toolkit/test-utils"; import { API } from "../src/CodeMirrorBlocks"; import { ASTNode } from "../src/ast"; -import { FunctionApp } from "../src/nodes"; +import { FunctionAppNode } from "../src/nodes"; const activeAriaId = (cmb: API) => cmb.getScrollerElement().getAttribute("aria-activedescendent"); @@ -193,12 +193,12 @@ describe("cut/copy/paste", () => { describe("tree navigation", () => { let cmb!: API; - let firstRoot: FunctionApp; + let firstRoot: FunctionAppNode; let secondRoot: ASTNode; - let thirdRoot: FunctionApp; + let thirdRoot: FunctionAppNode; let funcSymbol: ASTNode; let thirdArg: ASTNode; - let nestedExpr: FunctionApp; + let nestedExpr: FunctionAppNode; let lastNode: ASTNode; beforeEach(async () => { @@ -206,12 +206,12 @@ describe("tree navigation", () => { cmb.setValue("(+ 1 2 3) 99 (* 7 (* 1 2))"); const ast = cmb.getAst(); - firstRoot = ast.rootNodes[0] as FunctionApp; + firstRoot = ast.rootNodes[0] as FunctionAppNode; secondRoot = ast.rootNodes[1]; - thirdRoot = ast.rootNodes[2] as FunctionApp; + thirdRoot = ast.rootNodes[2] as FunctionAppNode; funcSymbol = firstRoot.fields.func; thirdArg = firstRoot.fields.args[2]; - nestedExpr = thirdRoot.fields.args[1] as FunctionApp; + nestedExpr = thirdRoot.fields.args[1] as FunctionAppNode; lastNode = nestedExpr.fields.args[1]; await finishRender(); }); @@ -374,7 +374,7 @@ describe("when dealing with node selection, ", () => { let cmb!: API; let literal1!: ASTNode; let literal2!: ASTNode; - let expr!: FunctionApp; + let expr!: FunctionAppNode; beforeEach(async () => { cmb = await mountCMB(wescheme); @@ -382,7 +382,7 @@ describe("when dealing with node selection, ", () => { const ast = cmb.getAst(); literal1 = ast.rootNodes[0]; literal2 = ast.rootNodes[1]; - expr = ast.rootNodes[2] as FunctionApp; + expr = ast.rootNodes[2] as FunctionAppNode; await finishRender(); }); diff --git a/packages/codemirror-blocks/spec/ast-test.ts b/packages/codemirror-blocks/spec/ast-test.ts index 5569961a1..20750c34d 100644 --- a/packages/codemirror-blocks/spec/ast-test.ts +++ b/packages/codemirror-blocks/spec/ast-test.ts @@ -1,12 +1,20 @@ import CodeMirror from "codemirror"; import { AST } from "../src/ast"; -import { FunctionApp, Literal, Sequence, Comment } from "../src/nodes"; +import { + FunctionApp, + Literal, + Sequence, + Comment, + FunctionAppNode, + LiteralNode, + SequenceNode, +} from "../src/nodes"; describe("The Literal Class", () => { it("should be constructed with a value and data type", () => { const from = { line: 0, ch: 0 }; const to = { line: 0, ch: 2 }; - const literal = new Literal(from, to, "11", "number"); + const literal = Literal(from, to, "11", "number"); expect(literal.from).toBe(from); expect(literal.to).toBe(to); expect(literal.fields.value).toBe("11"); @@ -15,17 +23,17 @@ describe("The Literal Class", () => { }); it("should set a default data type of unknown if one isn't provided", () => { - const literal = new Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"); + const literal = Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"); expect(literal.fields.dataType).toBe("unknown"); }); it("should only return itself when iterated over", () => { - const literal = new Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"); + const literal = Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"); expect([...literal.descendants()]).toEqual([literal]); }); it("should take an optional options parameter in its constructor", () => { - const literal = new Literal( + const literal = Literal( { line: 0, ch: 0 }, { line: 0, ch: 2 }, "11", @@ -37,27 +45,27 @@ describe("The Literal Class", () => { }); describe("The Sequence Class", () => { - let sequence: Sequence; - let expression1: FunctionApp; - let expression2: FunctionApp; + let sequence: SequenceNode; + let expression1: FunctionAppNode; + let expression2: FunctionAppNode; let from: CodeMirror.Position; let to: CodeMirror.Position; - let name: Literal; - let exprs: FunctionApp[]; + let name: LiteralNode; + let exprs: FunctionAppNode[]; beforeEach(() => { // (+ 1 2) - const func1 = new Literal( + const func1 = Literal( { line: 0, ch: 8 }, { line: 0, ch: 9 }, "+", "symbol" ); const args1 = [ - new Literal({ line: 0, ch: 10 }, { line: 0, ch: 11 }, "1"), - new Literal({ line: 0, ch: 12 }, { line: 0, ch: 13 }, "2"), + Literal({ line: 0, ch: 10 }, { line: 0, ch: 11 }, "1"), + Literal({ line: 0, ch: 12 }, { line: 0, ch: 13 }, "2"), ]; - expression1 = new FunctionApp( + expression1 = FunctionApp( { line: 0, ch: 7 }, { line: 0, ch: 14 }, func1, @@ -66,17 +74,17 @@ describe("The Sequence Class", () => { ); // (- 2 3) - const func2 = new Literal( + const func2 = Literal( { line: 0, ch: 16 }, { line: 0, ch: 17 }, "-", "symbol" ); const args2 = [ - new Literal({ line: 0, ch: 18 }, { line: 0, ch: 19 }, "2"), - new Literal({ line: 0, ch: 20 }, { line: 0, ch: 21 }, "3"), + Literal({ line: 0, ch: 18 }, { line: 0, ch: 19 }, "2"), + Literal({ line: 0, ch: 20 }, { line: 0, ch: 21 }, "3"), ]; - expression2 = new FunctionApp( + expression2 = FunctionApp( { line: 0, ch: 15 }, { line: 0, ch: 22 }, func2, @@ -87,14 +95,9 @@ describe("The Sequence Class", () => { // (begin (+ 1 2) (- 2 3)) from = { line: 0, ch: 0 }; to = { line: 0, ch: 23 }; - name = new Literal( - { line: 0, ch: 1 }, - { line: 0, ch: 6 }, - "begin", - "symbol" - ); + name = Literal({ line: 0, ch: 1 }, { line: 0, ch: 6 }, "begin", "symbol"); exprs = [expression1, expression2]; - sequence = new Sequence(from, to, exprs, name); + sequence = Sequence(from, to, exprs, name); }); it("should be constructed with a list of expressions", () => { @@ -106,25 +109,25 @@ describe("The Sequence Class", () => { it("should take an optional options parameter in its constructor", () => { const options = { "aria-label": "sequence" }; - const newSequence = new Sequence(from, to, exprs, name, options); + const newSequence = Sequence(from, to, exprs, name, options); expect(newSequence.options).toEqual(options); }); }); describe("The FunctionApp Class", () => { - let expression: FunctionApp; - let func: Literal; - let args: Literal[]; - let nestedExpression: FunctionApp; + let expression: FunctionAppNode; + let func: LiteralNode; + let args: LiteralNode[]; + let nestedExpression: FunctionAppNode; let ast: AST; beforeEach(() => { - func = new Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"); + func = Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"); args = [ - new Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), - new Literal({ line: 1, ch: 6 }, { line: 0, ch: 8 }, "22"), + Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), + Literal({ line: 1, ch: 6 }, { line: 0, ch: 8 }, "22"), ]; // (+ 11 22) - expression = new FunctionApp( + expression = FunctionApp( { line: 1, ch: 0 }, { line: 1, ch: 9 }, func, @@ -132,19 +135,19 @@ describe("The FunctionApp Class", () => { { "aria-label": "+ expression" } ); // (+ 11 (- 15 35)) - nestedExpression = new FunctionApp( + nestedExpression = FunctionApp( { line: 1, ch: 0 }, { line: 1, ch: 9 }, - new Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), + Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), [ - new Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), - new FunctionApp( + Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), + FunctionApp( { line: 1, ch: 0 }, { line: 1, ch: 9 }, - new Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "-", "symbol"), + Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "-", "symbol"), [ - new Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "15"), - new Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "35"), + Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "15"), + Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "35"), ] ), ] @@ -191,24 +194,24 @@ describe("The FunctionApp Class", () => { describe("The AST Class", () => { it("should take a set of root nodes in its constructor", () => { - const nodes = [new Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11")]; + const nodes = [Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11")]; const ast = new AST(nodes); expect(ast.rootNodes).toBe(nodes); }); it("should add every node to a node map for quick lookup", () => { const nodes = [ - new Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"), - new FunctionApp( + Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"), + FunctionApp( { line: 1, ch: 0 }, { line: 1, ch: 9 }, - new Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), + Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), [ - new Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), - new Literal({ line: 1, ch: 6 }, { line: 0, ch: 8 }, "22"), + Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), + Literal({ line: 1, ch: 6 }, { line: 0, ch: 8 }, "22"), ] ), - ] as [Literal, FunctionApp]; + ] as [LiteralNode, FunctionAppNode]; const ast = new AST(nodes); expect(ast.getNodeById(nodes[0].id)).toBe(nodes[0]); expect(ast.getNodeById(nodes[1].id)).toBe(nodes[1]); @@ -222,26 +225,26 @@ describe("The AST Class", () => { it("idential subtrees should have the same hash", () => { const nodes1 = [ - new Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"), - new FunctionApp( + Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11"), + FunctionApp( { line: 1, ch: 0 }, { line: 1, ch: 9 }, - new Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), + Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), [ - new Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), - new Literal({ line: 1, ch: 6 }, { line: 1, ch: 8 }, "22"), + Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), + Literal({ line: 1, ch: 6 }, { line: 1, ch: 8 }, "22"), ] ), ]; const nodes2 = [ - new Literal({ line: 1, ch: 0 }, { line: 1, ch: 2 }, "11"), - new FunctionApp( + Literal({ line: 1, ch: 0 }, { line: 1, ch: 2 }, "11"), + FunctionApp( { line: 2, ch: 0 }, { line: 2, ch: 9 }, - new Literal({ line: 2, ch: 1 }, { line: 2, ch: 2 }, "+", "symbol"), + Literal({ line: 2, ch: 1 }, { line: 2, ch: 2 }, "+", "symbol"), [ - new Literal({ line: 2, ch: 3 }, { line: 2, ch: 5 }, "11"), - new Literal({ line: 2, ch: 6 }, { line: 2, ch: 8 }, "22"), + Literal({ line: 2, ch: 3 }, { line: 2, ch: 5 }, "11"), + Literal({ line: 2, ch: 6 }, { line: 2, ch: 8 }, "22"), ] ), ]; @@ -252,28 +255,28 @@ describe("The AST Class", () => { it("idential subtrees with different comments should have different hashes", () => { const nodes1 = [ - new Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11", "Number", { - comment: new Comment({ line: 0, ch: 4 }, { line: 0, ch: 7 }, "moo"), + Literal({ line: 0, ch: 0 }, { line: 0, ch: 2 }, "11", "Number", { + comment: Comment({ line: 0, ch: 4 }, { line: 0, ch: 7 }, "moo"), }), - new FunctionApp( + FunctionApp( { line: 1, ch: 0 }, { line: 1, ch: 9 }, - new Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), + Literal({ line: 1, ch: 1 }, { line: 1, ch: 2 }, "+", "symbol"), [ - new Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), - new Literal({ line: 1, ch: 6 }, { line: 1, ch: 8 }, "22"), + Literal({ line: 1, ch: 3 }, { line: 1, ch: 5 }, "11"), + Literal({ line: 1, ch: 6 }, { line: 1, ch: 8 }, "22"), ] ), ]; const nodes2 = [ - new Literal({ line: 1, ch: 0 }, { line: 1, ch: 2 }, "11"), - new FunctionApp( + Literal({ line: 1, ch: 0 }, { line: 1, ch: 2 }, "11"), + FunctionApp( { line: 2, ch: 0 }, { line: 2, ch: 9 }, - new Literal({ line: 2, ch: 1 }, { line: 2, ch: 2 }, "+", "symbol"), + Literal({ line: 2, ch: 1 }, { line: 2, ch: 2 }, "+", "symbol"), [ - new Literal({ line: 2, ch: 3 }, { line: 2, ch: 5 }, "11"), - new Literal({ line: 2, ch: 6 }, { line: 2, ch: 8 }, "22"), + Literal({ line: 2, ch: 3 }, { line: 2, ch: 5 }, "11"), + Literal({ line: 2, ch: 6 }, { line: 2, ch: 8 }, "22"), ] ), ]; diff --git a/packages/codemirror-blocks/spec/drag-test.ts b/packages/codemirror-blocks/spec/drag-test.ts index 60b1d1078..02b701449 100644 --- a/packages/codemirror-blocks/spec/drag-test.ts +++ b/packages/codemirror-blocks/spec/drag-test.ts @@ -16,7 +16,7 @@ import { } from "../src/toolkit/test-utils"; import { API } from "../src/CodeMirrorBlocks"; import { ASTNode } from "../src/ast"; -import { FunctionApp } from "../src/nodes"; +import { FunctionAppNode } from "../src/nodes"; import { fireEvent } from "@testing-library/react"; describe("Drag and drop", () => { @@ -33,7 +33,7 @@ describe("Drag and drop", () => { let dropTargetEls: NodeListOf; const retrieve = () => { - const funcApp = cmb.getAst().rootNodes[0] as FunctionApp; + const funcApp = cmb.getAst().rootNodes[0] as FunctionAppNode; firstArg = funcApp.fields.args[0]; secondArg = funcApp.fields.args[1]; dropTargetEls = cmb @@ -193,7 +193,7 @@ describe("Drag and drop", () => { fireEvent(lastDropTarget, drop()); await finishRender(); retrieve(); - const newFirstRoot = cmb.getAst().rootNodes[0] as FunctionApp; + const newFirstRoot = cmb.getAst().rootNodes[0] as FunctionAppNode; const newLastChild = newFirstRoot.fields.args[2]; expect(cmb.getValue()).toBe("\n(+ 1 2 (collapse me))"); expect(newFirstRoot.element!.getAttribute("aria-expanded")).toBe("true"); diff --git a/packages/codemirror-blocks/spec/focus-test.ts b/packages/codemirror-blocks/spec/focus-test.ts index ce41f24a8..812e6cf7a 100644 --- a/packages/codemirror-blocks/spec/focus-test.ts +++ b/packages/codemirror-blocks/spec/focus-test.ts @@ -1,7 +1,7 @@ import { ASTNode } from "../src/ast"; import { API } from "../src/CodeMirrorBlocks"; import wescheme from "../src/languages/wescheme"; -import { FunctionApp, Literal } from "../src/nodes"; +import type { FunctionAppNode, LiteralNode } from "../src/nodes"; /*eslint no-unused-vars: "off"*/ import { @@ -19,7 +19,7 @@ debugLog("Doing focus-test.js"); describe("focusing,", () => { let cmb!: API; - let expression!: FunctionApp; + let expression!: FunctionAppNode; let func!: ASTNode; let literal1!: ASTNode; let literal2!: ASTNode; @@ -29,7 +29,7 @@ describe("focusing,", () => { cmb = await mountCMB(wescheme); cmb.setValue("(+ 1 2 3)"); await finishRender(); - expression = cmb.getAst().rootNodes[0] as FunctionApp; + expression = cmb.getAst().rootNodes[0] as FunctionAppNode; func = expression.fields.func; literal1 = expression.fields.args[0]; literal2 = expression.fields.args[1]; @@ -102,7 +102,7 @@ describe("focusing,", () => { // there's an extra space inserted after 99 expect(cmb.getValue()).toBe("(+ 1 99 2 3)"); // TODO(Emmanuel): does getFocusedNode().value always return strings? - expect((cmb.getFocusedNode() as Literal).fields.value).toBe("99"); + expect((cmb.getFocusedNode() as LiteralNode).fields.value).toBe("99"); }); it("inserting multiple nodes should put focus on the last of the new nodes", async () => { @@ -115,6 +115,6 @@ describe("focusing,", () => { await finishRender(); expect(cmb.getValue()).toBe("(+ 1 99 88 77 2 3)"); // TODO(Emmanuel): does getFocusedNode().value always return strings? - expect((cmb.getFocusedNode() as Literal).fields.value).toBe("77"); + expect((cmb.getFocusedNode() as LiteralNode).fields.value).toBe("77"); }); }); diff --git a/packages/codemirror-blocks/spec/new-blocks-test.ts b/packages/codemirror-blocks/spec/new-blocks-test.ts index 63f2d51c6..05adecdd6 100644 --- a/packages/codemirror-blocks/spec/new-blocks-test.ts +++ b/packages/codemirror-blocks/spec/new-blocks-test.ts @@ -2,7 +2,7 @@ import CodeMirror from "codemirror"; import { AST, ASTNode } from "../src/ast"; import type { API } from "../src/CodeMirrorBlocks"; import wescheme from "../src/languages/wescheme"; -import { FunctionApp } from "../src/nodes"; +import { FunctionAppNode } from "../src/nodes"; import { teardown, @@ -128,7 +128,7 @@ describe("The CodeMirrorBlocks Class", function () { cmb.setValue("()"); await finishRender(); cmb.getValue("(...)"); // blank should be inserted by parser, as '...' - const blank = (cmb.getAst().rootNodes[0] as FunctionApp).fields.func; + const blank = (cmb.getAst().rootNodes[0] as FunctionAppNode).fields.func; click(blank.element!); await finishRender(); expect(blank.isEditable()).toBe(true); @@ -187,16 +187,16 @@ describe("The CodeMirrorBlocks Class", function () { let firstRoot!: ASTNode; let firstArg!: ASTNode; let whiteSpaceEl!: Element; - let blank!: FunctionApp; + let blank!: FunctionAppNode; beforeEach(async function () { cmb.setValue("(+ 1 2) (+)"); await finishRender(); ast = cmb.getAst(); firstRoot = ast.rootNodes[0]; - firstArg = (ast.rootNodes[0] as FunctionApp).fields.args[0]; + firstArg = (ast.rootNodes[0] as FunctionAppNode).fields.args[0]; whiteSpaceEl = firstArg.element!.nextElementSibling!; - blank = ast.rootNodes[1] as FunctionApp; + blank = ast.rootNodes[1] as FunctionAppNode; }); it("Ctrl-[ should jump to the left of a top-level node", function () { diff --git a/packages/codemirror-blocks/spec/performEdits-test.ts b/packages/codemirror-blocks/spec/performEdits-test.ts index 4297f5c8b..19efc3d0c 100644 --- a/packages/codemirror-blocks/spec/performEdits-test.ts +++ b/packages/codemirror-blocks/spec/performEdits-test.ts @@ -11,7 +11,6 @@ import { performEdits, } from "../src/edits/performEdits"; import wescheme from "../src/languages/wescheme"; -import { FunctionApp } from "../src/nodes"; import { AppStore, createAppStore } from "../src/store"; let editor!: CodeMirrorFacade; @@ -106,7 +105,7 @@ foo it("InsertChildEdit replaces a node with some text", () => { const node = [...ast.rootNodes[1].children()][1]; - expect(node).toBeInstanceOf(FunctionApp); + expect(node.type).toBe("functionApp"); const edit = edit_insert("foo", node, "args", { line: 2, ch: 19 }); expect(edit.toString()).toEqual("InsertChild 2:19-2:19"); @@ -122,7 +121,7 @@ foo it("Combines multiple edits together", () => { const children = [...ast.rootNodes[1].children()]; const node = children[1]; - expect(node).toBeInstanceOf(FunctionApp); + expect(node.type).toBe("functionApp"); const edits = [ edit_insert("foo", node, "args", { line: 2, ch: 19 }), diff --git a/packages/codemirror-blocks/spec/ui/Toolbar-test.tsx b/packages/codemirror-blocks/spec/ui/Toolbar-test.tsx index b8b517b35..60eafd5dc 100644 --- a/packages/codemirror-blocks/spec/ui/Toolbar-test.tsx +++ b/packages/codemirror-blocks/spec/ui/Toolbar-test.tsx @@ -16,11 +16,7 @@ fdescribe("Toolbar", () => { name: "some lang", parse: jest.fn(), getASTNodeForPrimitive: (primitive: Primitive) => { - return new Literal( - { line: 0, ch: 0 }, - { line: 0, ch: 0 }, - primitive.name - ); + return Literal({ line: 0, ch: 0 }, { line: 0, ch: 0 }, primitive.name); }, }); primitives = PrimitiveGroup.fromConfig("some-lang-id", { diff --git a/packages/codemirror-blocks/src/CodeMirrorBlocks.tsx b/packages/codemirror-blocks/src/CodeMirrorBlocks.tsx index b3aca4347..f8e42c7ab 100644 --- a/packages/codemirror-blocks/src/CodeMirrorBlocks.tsx +++ b/packages/codemirror-blocks/src/CodeMirrorBlocks.tsx @@ -71,7 +71,9 @@ export type Language = { /** * A function for generating a Literal ast node from a Primitive */ - getLiteralNodeForPrimitive?: (primitive: Primitive) => Literal; + getLiteralNodeForPrimitive?: ( + primitive: Primitive + ) => ReturnType; /** * Returns a list of language primitives that will be displayed diff --git a/packages/codemirror-blocks/src/ast.ts b/packages/codemirror-blocks/src/ast.ts index f40cc3975..5bb3bd35b 100644 --- a/packages/codemirror-blocks/src/ast.ts +++ b/packages/codemirror-blocks/src/ast.ts @@ -7,7 +7,6 @@ import { genUniqueId, } from "./utils"; import * as P from "pretty-fast-pretty-printer"; -import type { Comment } from "./nodes"; import type { NodeSpec } from "./nodeSpec"; import type React from "react"; import type { Props as NodeProps } from "./components/Node"; @@ -526,7 +525,7 @@ export type Range = { }; export type NodeOptions = { - comment?: Comment; + comment?: ASTNode<{ comment: string }>; "aria-label"?: string; /** * A predicate, which prevents the node from being edited diff --git a/packages/codemirror-blocks/src/languages/example/ExampleParser.ts b/packages/codemirror-blocks/src/languages/example/ExampleParser.ts index 9f981e4d5..f6eefdc04 100644 --- a/packages/codemirror-blocks/src/languages/example/ExampleParser.ts +++ b/packages/codemirror-blocks/src/languages/example/ExampleParser.ts @@ -157,7 +157,7 @@ export default class ExampleParser { parseLiteral() { const literalToken = this.getToken(); - return new Literal( + return Literal( literalToken.from, literalToken.to, literalToken.text, @@ -179,10 +179,10 @@ export default class ExampleParser { args.push(this.parseNextToken()); } const closeParenToken = this.getToken(); - return new FunctionApp( + return FunctionApp( token.from, closeParenToken.to, - new Literal( + Literal( identifierToken.from, identifierToken.to, identifierToken.text, diff --git a/packages/codemirror-blocks/src/languages/wescheme/WeschemeParser.js b/packages/codemirror-blocks/src/languages/wescheme/WeschemeParser.js index 4b1326795..ba829f497 100644 --- a/packages/codemirror-blocks/src/languages/wescheme/WeschemeParser.js +++ b/packages/codemirror-blocks/src/languages/wescheme/WeschemeParser.js @@ -387,7 +387,7 @@ function parseNode(node, i) { ); } else if (node instanceof structures.unsupportedExpr) { if (node.val.constructor !== Array) return null; - let unknown = new Unknown( + let unknown = Unknown( from, to, node.val.map(parseNode).filter((item) => item !== null), diff --git a/packages/codemirror-blocks/src/nodes.tsx b/packages/codemirror-blocks/src/nodes.tsx index c11ccbd81..a34018d8b 100644 --- a/packages/codemirror-blocks/src/nodes.tsx +++ b/packages/codemirror-blocks/src/nodes.tsx @@ -32,613 +32,575 @@ function withComment( } } -export class Unknown extends ASTNode<{ elts: ASTNode[] }> { - constructor(from: Pos, to: Pos, elts: ASTNode[], options: NodeOptions = {}) { - super({ - from, - to, - type: "unknown", - fields: { elts }, - options, - pretty: (node) => - withComment( - P.standardSexpr(node.fields.elts[0], node.fields.elts.slice(1)), - node.options.comment, - node - ), - render(props) { - const firstElt = props.node.fields.elts[0].reactElement(); - const restElts = props.node.fields.elts.slice(1); - return ( - - {firstElt} - - {restElts} - - - ); - }, - longDescription(node, level) { - return ( - `an unknown expression with ${pluralize( - "children", - node.fields.elts - )} ` + +export function Unknown( + from: Pos, + to: Pos, + elts: ASTNode[], + options: NodeOptions = {} +) { + return new ASTNode({ + from, + to, + type: "unknown", + fields: { elts }, + options, + pretty: (node) => + withComment( + P.standardSexpr(node.fields.elts[0], node.fields.elts.slice(1)), + node.options.comment, + node + ), + render(props) { + const firstElt = props.node.fields.elts[0].reactElement(); + const restElts = props.node.fields.elts.slice(1); + return ( + + {firstElt} + + {restElts} + + + ); + }, + longDescription(node, level) { + return ( + `an unknown expression with ${pluralize( + "children", node.fields.elts + )} ` + + node.fields.elts + .map( + (e, i, elts) => + (elts.length > 1 ? i + 1 + ": " : "") + e.describe(level) + ) + .join(", ") + ); + }, + spec: Spec.nodeSpec([Spec.list("elts")]), + }); +} + +export type FunctionAppNode = ReturnType; +export function FunctionApp( + from: Pos, + to: Pos, + func: ASTNode, + args: ASTNode[], + options: NodeOptions = {} +) { + return new ASTNode({ + from, + to, + type: "functionApp", + fields: { func, args }, + options, + pretty: (node) => + withComment( + P.standardSexpr(node.fields.func, node.fields.args), + node.options.comment, + node + ), + render(props) { + return ( + + + {props.node.fields.func.reactElement()} + + + {props.node.fields.args} + + + ); + }, + longDescription(node, level) { + // if it's the top level, enumerate the args + if (node.level - level == 0) { + return ( + `applying the function ${node.fields.func.describe( + level + )} to ${pluralize("argument", node.fields.args)} ` + + node.fields.args .map( - (e, i, elts) => - (elts.length > 1 ? i + 1 + ": " : "") + e.describe(level) + (a, i, args) => + (args.length > 1 ? i + 1 + ": " : "") + a.describe(level) ) .join(", ") ); - }, - spec: Spec.nodeSpec([Spec.list("elts")]), - }); - } -} - -export class FunctionApp extends ASTNode<{ func: ASTNode; args: ASTNode[] }> { - constructor( - from: Pos, - to: Pos, - func: ASTNode, - args: ASTNode[], - options: NodeOptions = {} - ) { - super({ - from, - to, - type: "functionApp", - fields: { func, args }, - options, - pretty: (node) => - withComment( - P.standardSexpr(node.fields.func, node.fields.args), - node.options.comment, - node - ), - render(props) { + } + // if we're lower than that (but not so low that `.shortDescription()` is used), use "f of A, B, C" format + else return ( - - - {props.node.fields.func.reactElement()} - - - {props.node.fields.args} - - + `${node.fields.func.describe(level)} of ` + + node.fields.args.map((a) => a.describe(level)).join(", ") ); - }, - longDescription(node, level) { - // if it's the top level, enumerate the args - if (node.level - level == 0) { - return ( - `applying the function ${node.fields.func.describe( - level - )} to ${pluralize("argument", node.fields.args)} ` + - node.fields.args - .map( - (a, i, args) => - (args.length > 1 ? i + 1 + ": " : "") + a.describe(level) - ) - .join(", ") - ); - } - // if we're lower than that (but not so low that `.shortDescription()` is used), use "f of A, B, C" format - else - return ( - `${node.fields.func.describe(level)} of ` + - node.fields.args.map((a) => a.describe(level)).join(", ") - ); - }, - spec: Spec.nodeSpec([Spec.required("func"), Spec.list("args")]), - }); - } + }, + spec: Spec.nodeSpec([Spec.required("func"), Spec.list("args")]), + }); } -export class IdentifierList extends ASTNode<{ kind: string; ids: ASTNode[] }> { - constructor( - from: Pos, - to: Pos, - kind: string, - ids: ASTNode[], - options: NodeOptions = {} - ) { - super({ - from, - to, - type: "identifierList", - fields: { kind, ids }, - options, - pretty: (node) => - withComment(P.sepBy(node.fields.ids, " "), node.options.comment, node), - render(props) { - return ( - - - {props.node.fields.ids} - - - ); - }, - longDescription(node, level) { - return enumerateList(node.fields.ids, level); - }, - spec: Spec.nodeSpec([Spec.value("kind"), Spec.list("ids")]), - }); - } +export function IdentifierList( + from: Pos, + to: Pos, + kind: string, + ids: ASTNode[], + options: NodeOptions = {} +) { + return new ASTNode({ + from, + to, + type: "identifierList", + fields: { kind, ids }, + options, + pretty: (node) => + withComment(P.sepBy(node.fields.ids, " "), node.options.comment, node), + render(props) { + return ( + + + {props.node.fields.ids} + + + ); + }, + longDescription(node, level) { + return enumerateList(node.fields.ids, level); + }, + spec: Spec.nodeSpec([Spec.value("kind"), Spec.list("ids")]), + }); } -export class StructDefinition extends ASTNode<{ - name: ASTNode; - fields: ASTNode; -}> { - constructor( - from: Pos, - to: Pos, - name: ASTNode, - fields: ASTNode, - options: NodeOptions = {} - ) { - super({ - from, - to, - type: "structDefinition", - fields: { name, fields }, - options, - pretty: (node) => - withComment( - P.lambdaLikeSexpr( - "define-struct", - node.fields.name, - P.horz("(", node.fields.fields, ")") - ), - node.options.comment, - node +export function StructDefinition( + from: Pos, + to: Pos, + name: ASTNode, + fields: ASTNode, + options: NodeOptions = {} +) { + return new ASTNode({ + from, + to, + type: "structDefinition", + fields: { name, fields }, + options, + pretty: (node) => + withComment( + P.lambdaLikeSexpr( + "define-struct", + node.fields.name, + P.horz("(", node.fields.fields, ")") ), - render(props) { - const name = props.node.fields.name.reactElement(); - const fields = props.node.fields.fields.reactElement(); - return ( - - - define-struct - {name} - - {fields} - - ); - }, - longDescription(node, level) { - return `define ${node.fields.name.describe( - level - )} to be a structure with ${node.fields.fields.describe(level)}`; - }, - spec: Spec.nodeSpec([Spec.value("name"), Spec.required("fields")]), - }); - } + node.options.comment, + node + ), + render(props) { + const name = props.node.fields.name.reactElement(); + const fields = props.node.fields.fields.reactElement(); + return ( + + + define-struct + {name} + + {fields} + + ); + }, + longDescription(node, level) { + return `define ${node.fields.name.describe( + level + )} to be a structure with ${node.fields.fields.describe(level)}`; + }, + spec: Spec.nodeSpec([Spec.value("name"), Spec.required("fields")]), + }); } -export class VariableDefinition extends ASTNode<{ - name: ASTNode; - body: ASTNode; -}> { - constructor(from: Pos, to: Pos, name: ASTNode, body: ASTNode, options = {}) { - super({ - from, - to, - type: "variableDefinition", - fields: { name, body }, - options, - pretty: (node) => - withComment( - P.lambdaLikeSexpr("define", node.fields.name, node.fields.body), - node.options.comment, - node - ), - render(props) { - const body = props.node.fields.body.reactElement(); - const name = props.node.fields.name.reactElement(); - return ( - - - define - {name} - - {body} - - ); - }, - longDescription(node, level) { - const insert = ["literal", "blank"].includes(node.fields.body.type) - ? "" - : "the result of:"; - return `define ${ - node.fields.name - } to be ${insert} ${node.fields.body.describe(level)}`; - }, - spec: Spec.nodeSpec([Spec.required("name"), Spec.required("body")]), - }); - } +export function VariableDefinition( + from: Pos, + to: Pos, + name: ASTNode, + body: ASTNode, + options = {} +) { + return new ASTNode({ + from, + to, + type: "variableDefinition", + fields: { name, body }, + options, + pretty: (node) => + withComment( + P.lambdaLikeSexpr("define", node.fields.name, node.fields.body), + node.options.comment, + node + ), + render(props) { + const body = props.node.fields.body.reactElement(); + const name = props.node.fields.name.reactElement(); + return ( + + + define + {name} + + {body} + + ); + }, + longDescription(node, level) { + const insert = ["literal", "blank"].includes(node.fields.body.type) + ? "" + : "the result of:"; + return `define ${ + node.fields.name + } to be ${insert} ${node.fields.body.describe(level)}`; + }, + spec: Spec.nodeSpec([Spec.required("name"), Spec.required("body")]), + }); } -export class LambdaExpression extends ASTNode<{ - body: ASTNode; - args: IdentifierList; -}> { - constructor( - from: Pos, - to: Pos, - args: IdentifierList, - body: ASTNode, - options = {} - ) { - super({ - from, - to, - type: "lambdaExpression", - fields: { body, args }, - options, - pretty: (node) => - P.lambdaLikeSexpr( - "lambda(", - P.horz("(", node.fields.args, ")"), - node.fields.body - ), - render(props) { - const args = props.node.fields.args.reactElement(); - const body = props.node.fields.body.reactElement(); - return ( - - λ ({args}) - {body} - - ); - }, - longDescription(node, level) { - return `an anonymous function of ${pluralize( - "argument", - node.fields.args.fields.ids - )}: +export function LambdaExpression( + from: Pos, + to: Pos, + args: ReturnType, + body: ASTNode, + options = {} +) { + return new ASTNode({ + from, + to, + type: "lambdaExpression", + fields: { body, args }, + options, + pretty: (node) => + P.lambdaLikeSexpr( + "lambda(", + P.horz("(", node.fields.args, ")"), + node.fields.body + ), + render(props) { + const args = props.node.fields.args.reactElement(); + const body = props.node.fields.body.reactElement(); + return ( + + λ ({args}) + {body} + + ); + }, + longDescription(node, level) { + return `an anonymous function of ${pluralize( + "argument", + node.fields.args.fields.ids + )}: ${node.fields.args.describe(level)}, with body: ${node.fields.body.describe(level)}`; - }, - spec: Spec.nodeSpec([Spec.required("args"), Spec.required("body")]), - }); - } + }, + spec: Spec.nodeSpec([Spec.required("args"), Spec.required("body")]), + }); } -export class FunctionDefinition extends ASTNode<{ - name: ASTNode; - params: ASTNode; - body: ASTNode; -}> { - constructor( - from: Pos, - to: Pos, - name: ASTNode, - params: ASTNode, - body: ASTNode, - options = {} - ) { - super({ - from, - to, - type: "functionDefinition", - fields: { name, params, body }, - options, - pretty: (node) => - withComment( - P.lambdaLikeSexpr( - "define", - P.standardSexpr(node.fields.name, [node.fields.params]), - node.fields.body - ), - node.options.comment, - node +export function FunctionDefinition( + from: Pos, + to: Pos, + name: ASTNode, + params: ASTNode, + body: ASTNode, + options = {} +) { + return new ASTNode({ + from, + to, + type: "functionDefinition", + fields: { name, params, body }, + options, + pretty: (node) => + withComment( + P.lambdaLikeSexpr( + "define", + P.standardSexpr(node.fields.name, [node.fields.params]), + node.fields.body ), - render(props) { - const params = props.node.fields.params.reactElement(); - const body = props.node.fields.body.reactElement(); - const name = props.node.fields.name.reactElement(); - return ( - - - define ({name} {params}) - - {body} - - ); - }, - longDescription(node, level) { - return `define ${node.fields.name} to be a function of + node.options.comment, + node + ), + render(props) { + const params = props.node.fields.params.reactElement(); + const body = props.node.fields.body.reactElement(); + const name = props.node.fields.name.reactElement(); + return ( + + + define ({name} {params}) + + {body} + + ); + }, + longDescription(node, level) { + return `define ${node.fields.name} to be a function of ${node.fields.params.describe(level)}, with body: ${node.fields.body.describe(level)}`; - }, - spec: Spec.nodeSpec([ - Spec.required("name"), - Spec.required("params"), - Spec.required("body"), - ]), - }); - } + }, + spec: Spec.nodeSpec([ + Spec.required("name"), + Spec.required("params"), + Spec.required("body"), + ]), + }); } -export class CondClause extends ASTNode<{ - testExpr: ASTNode; - thenExprs: ASTNode[]; -}> { - constructor( - from: Pos, - to: Pos, - testExpr: ASTNode, - thenExprs: ASTNode[], - options = {} - ) { - super({ - from, - to, - type: "condClause", - fields: { testExpr, thenExprs }, - options, - pretty: (node) => - P.horz( - "[", - P.sepBy([node.fields.testExpr].concat(node.fields.thenExprs), " "), - "]" - ), - render(props) { - const testExpr = props.node.fields.testExpr.reactElement(); - return ( - -
-
{testExpr}
-
- {props.node.fields.thenExprs.map((thenExpr, index) => ( - - - {thenExpr.reactElement()} - - ))} - -
+export function CondClause( + from: Pos, + to: Pos, + testExpr: ASTNode, + thenExprs: ASTNode[], + options = {} +) { + return new ASTNode({ + from, + to, + type: "condClause", + fields: { testExpr, thenExprs }, + options, + pretty: (node) => + P.horz( + "[", + P.sepBy([node.fields.testExpr].concat(node.fields.thenExprs), " "), + "]" + ), + render(props) { + const testExpr = props.node.fields.testExpr.reactElement(); + return ( + +
+
{testExpr}
+
+ {props.node.fields.thenExprs.map((thenExpr, index) => ( + + + {thenExpr.reactElement()} + + ))} +
- - ); - }, - longDescription(node, level) { - return `condition: if ${node.fields.testExpr.describe( - level - )}, then, ${node.fields.thenExprs.map((te) => te.describe(level))}`; - }, - spec: Spec.nodeSpec([Spec.required("testExpr"), Spec.list("thenExprs")]), - }); - } +
+
+ ); + }, + longDescription(node, level) { + return `condition: if ${node.fields.testExpr.describe( + level + )}, then, ${node.fields.thenExprs.map((te) => te.describe(level))}`; + }, + spec: Spec.nodeSpec([Spec.required("testExpr"), Spec.list("thenExprs")]), + }); } -export class CondExpression extends ASTNode<{ clauses: ASTNode[] }> { - constructor(from: Pos, to: Pos, clauses: ASTNode[], options = {}) { - super({ - from, - to, - type: "condExpression", - fields: { clauses }, - options, - pretty: (node) => P.beginLikeSexpr("cond", node.fields.clauses), - render(props) { - const clauses = props.node.fields.clauses.map((clause, index) => - clause.reactElement({ key: index }) - ); - return ( - - cond -
{clauses}
-
- ); - }, - longDescription(node, level) { - return `a conditional expression with ${pluralize( - "condition", - node.fields.clauses - )}: +export function CondExpression( + from: Pos, + to: Pos, + clauses: ASTNode[], + options = {} +) { + return new ASTNode({ + from, + to, + type: "condExpression", + fields: { clauses }, + options, + pretty: (node) => P.beginLikeSexpr("cond", node.fields.clauses), + render(props) { + const clauses = props.node.fields.clauses.map((clause, index) => + clause.reactElement({ key: index }) + ); + return ( + + cond +
{clauses}
+
+ ); + }, + longDescription(node, level) { + return `a conditional expression with ${pluralize( + "condition", + node.fields.clauses + )}: ${node.fields.clauses.map((c) => c.describe(level))}`; - }, - spec: Spec.nodeSpec([Spec.list("clauses")]), - }); - } + }, + spec: Spec.nodeSpec([Spec.list("clauses")]), + }); } -export class IfExpression extends ASTNode<{ - testExpr: ASTNode; - thenExpr: ASTNode; - elseExpr: ASTNode; -}> { - constructor( - from: Pos, - to: Pos, - testExpr: ASTNode, - thenExpr: ASTNode, - elseExpr: ASTNode, - options = {} - ) { - super({ - from, - to, - type: "ifExpression", - fields: { testExpr, thenExpr, elseExpr }, - options, - pretty: (node) => - withComment( - P.standardSexpr("if", [ - node.fields.testExpr, - node.fields.thenExpr, - node.fields.elseExpr, - ]), - node.options.comment, - node - ), - render(props) { - const testExpr = props.node.fields.testExpr.reactElement(); - const thenExpr = props.node.fields.thenExpr.reactElement(); - const elseExpr = props.node.fields.elseExpr.reactElement(); - return ( - - if -
-
-
{testExpr}
-
{thenExpr}
-
-
-
- else -
-
{elseExpr}
-
+export function IfExpression( + from: Pos, + to: Pos, + testExpr: ASTNode, + thenExpr: ASTNode, + elseExpr: ASTNode, + options = {} +) { + return new ASTNode({ + from, + to, + type: "ifExpression", + fields: { testExpr, thenExpr, elseExpr }, + options, + pretty: (node) => + withComment( + P.standardSexpr("if", [ + node.fields.testExpr, + node.fields.thenExpr, + node.fields.elseExpr, + ]), + node.options.comment, + node + ), + render(props) { + const testExpr = props.node.fields.testExpr.reactElement(); + const thenExpr = props.node.fields.thenExpr.reactElement(); + const elseExpr = props.node.fields.elseExpr.reactElement(); + return ( + + if +
+
+
{testExpr}
+
{thenExpr}
- - ); - }, - longDescription(node, level) { - return ( - `an if expression: if ${node.fields.testExpr.describe( - level - )}, then ${node.fields.thenExpr.describe(level)} ` + - `else ${node.fields.elseExpr.describe(level)}` - ); - }, - spec: Spec.nodeSpec([ - Spec.required("testExpr"), - Spec.required("thenExpr"), - Spec.required("elseExpr"), - ]), - }); - } +
+
else
+
{elseExpr}
+
+
+
+ ); + }, + longDescription(node, level) { + return ( + `an if expression: if ${node.fields.testExpr.describe( + level + )}, then ${node.fields.thenExpr.describe(level)} ` + + `else ${node.fields.elseExpr.describe(level)}` + ); + }, + spec: Spec.nodeSpec([ + Spec.required("testExpr"), + Spec.required("thenExpr"), + Spec.required("elseExpr"), + ]), + }); } -export class Literal extends ASTNode<{ value: string; dataType: string }> { - constructor( - from: Pos, - to: Pos, - value: string, - dataType = "unknown", - options = {} - ) { - super({ - from, - to, - type: "literal", - fields: { value, dataType }, - options, - pretty: (node) => - withComment(P.txt(node.fields.value), node.options.comment, node), - render(props) { - return ( - - - {props.node.fields.value} - - - ); - }, - spec: Spec.nodeSpec([Spec.value("value"), Spec.value("dataType")]), - }); - } +export type LiteralNode = ReturnType; +export function Literal( + from: Pos, + to: Pos, + value: string, + dataType = "unknown", + options = {} +) { + return new ASTNode({ + from, + to, + type: "literal", + fields: { value, dataType }, + options, + pretty: (node) => + withComment(P.txt(node.fields.value), node.options.comment, node), + render(props) { + return ( + + + {props.node.fields.value} + + + ); + }, + spec: Spec.nodeSpec([Spec.value("value"), Spec.value("dataType")]), + }); } -export class Comment extends ASTNode<{ comment: string }> { - constructor(from: Pos, to: Pos, comment: string, options = {}) { - super({ - from, - to, - type: "comment", - fields: { comment }, - options: { isLockedP: true, ...options }, - pretty: (node) => { - const words = node.fields.comment.trim().split(/\s+/); - const wrapped = P.wrap(words); - // Normalize all comments to block comments - return P.concat("#| ", wrapped, " |#"); - }, - render(props) { - // eslint-disable-line no-unused-vars - return ( - - ); - }, - spec: Spec.nodeSpec([Spec.value("comment")]), - }); - } +export function Comment(from: Pos, to: Pos, comment: string, options = {}) { + return new ASTNode({ + from, + to, + type: "comment", + fields: { comment }, + options: { isLockedP: true, ...options }, + pretty: (node) => { + const words = node.fields.comment.trim().split(/\s+/); + const wrapped = P.wrap(words); + // Normalize all comments to block comments + return P.concat("#| ", wrapped, " |#"); + }, + render(props) { + // eslint-disable-line no-unused-vars + return ( + + ); + }, + spec: Spec.nodeSpec([Spec.value("comment")]), + }); } -export class Blank extends ASTNode<{ value: string; dataType: string }> { - constructor( - from: Pos, - to: Pos, - value: string, - dataType = "blank", - options = {} - ) { - super({ - from, - to, - type: "blank", - fields: { value: value || "...", dataType }, - options, - pretty: (node) => P.txt(node.fields.value), - render(props) { - return ( - - - - ); - }, - spec: Spec.nodeSpec([Spec.value("value"), Spec.value("dataType")]), - }); - } +export function Blank( + from: Pos, + to: Pos, + value: string, + dataType = "blank", + options = {} +) { + return new ASTNode({ + from, + to, + type: "blank", + fields: { value: value || "...", dataType }, + options, + pretty: (node) => P.txt(node.fields.value), + render(props) { + return ( + + + + ); + }, + spec: Spec.nodeSpec([Spec.value("value"), Spec.value("dataType")]), + }); } -export class Sequence extends ASTNode<{ name: ASTNode; exprs: ASTNode[] }> { - constructor( - from: Pos, - to: Pos, - exprs: ASTNode[], - name: ASTNode, - options = {} - ) { - super({ - from, - to, - type: "sequence", - fields: { name, exprs }, - options, - pretty: (node) => P.vert(node.fields.name, ...node.fields.exprs), - render(props) { - return ( - - - {props.node.fields.name.reactElement()} - - - {props.node.fields.exprs} - - - ); - }, - longDescription(node, level) { - return `a sequence containing ${enumerateList( - node.fields.exprs, - level - )}`; - }, - spec: Spec.nodeSpec([Spec.optional("name"), Spec.list("exprs")]), - }); - } +export type SequenceNode = ReturnType; +export function Sequence( + from: Pos, + to: Pos, + exprs: ASTNode[], + name: ASTNode, + options = {} +) { + return new ASTNode({ + from, + to, + type: "sequence", + fields: { name, exprs }, + options, + pretty: (node) => P.vert(node.fields.name, ...node.fields.exprs), + render(props) { + return ( + + + {props.node.fields.name.reactElement()} + + + {props.node.fields.exprs} + + + ); + }, + longDescription(node, level) { + return `a sequence containing ${enumerateList(node.fields.exprs, level)}`; + }, + spec: Spec.nodeSpec([Spec.optional("name"), Spec.list("exprs")]), + }); }