diff --git a/package-lock.json b/package-lock.json index d6bd844d..83afb9ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,12 @@ "name": "web-ide", "version": "0.1.0", "dependencies": { - "@csound/browser": "^7.0.0-beta29", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.10.4", + "@codemirror/language": "^6.12.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.8", + "@csound/browser": "^7.0.0-beta32", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@emotion/core": "^11.0.0", @@ -16,11 +21,13 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@hello-pangea/dnd": "^18.0.1", - "@hlolli/codemirror-lang-csound": "^1.0.0-alpha10", + "@hlolli/codemirror-lang-csound": "^1.0.0-alpha11", + "@lezer/common": "^1.5.2", "@mui/icons-material": "^6.2.0", "@mui/material": "^6.2.0", "@reduxjs/toolkit": "^2.5.0", "@vitejs/plugin-react": "^4.3.4", + "codemirror": "^6.0.2", "date-fns": "^4.1.0", "firebase-functions": "^6.4.0", "history": "5.3.0", @@ -84,7 +91,6 @@ "babel-loader": "9.2.1", "babel-plugin-named-asset-import": "^0.3.8", "camelcase": "^8.0.0", - "codemirror": "^6.0.1", "concurrently": "^9.1.0", "cross-env": "^7.0.3", "css-loader": "7.1.2", @@ -1983,101 +1989,90 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.3", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.3.tgz", - "integrity": "sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==", + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" - }, - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" } }, "node_modules/@codemirror/commands": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", - "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", + "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.7.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "node_modules/@codemirror/language": { - "version": "6.10.6", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.6.tgz", - "integrity": "sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==", + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", + "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "node_modules/@codemirror/lint": { - "version": "6.8.4", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz", - "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==", + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.35.0", + "@codemirror/view": "^6.42.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/search": { - "version": "6.5.8", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.8.tgz", - "integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/state": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", - "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, - "node_modules/@codemirror/text": { - "version": "0.19.6", - "resolved": "https://registry.npmjs.org/@codemirror/text/-/text-0.19.6.tgz", - "integrity": "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA==", - "license": "MIT" - }, "node_modules/@codemirror/view": { - "version": "6.35.3", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.35.3.tgz", - "integrity": "sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==", + "version": "6.43.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.8.tgz", + "integrity": "sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==", "license": "MIT", "dependencies": { - "@codemirror/state": "^6.5.0", + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "node_modules/@csound/browser": { - "version": "7.0.0-beta29", - "resolved": "https://registry.npmjs.org/@csound/browser/-/browser-7.0.0-beta29.tgz", - "integrity": "sha512-3yHflHJS6mVTi4LAdXGZOBIgs9eQHrQqNCEoF38Q+DnOspEUUb3YIT7VjzlA4AkFa7rNMzT0zAgwqucd1nfFlQ==", + "version": "7.0.0-beta32", + "resolved": "https://registry.npmjs.org/@csound/browser/-/browser-7.0.0-beta32.tgz", + "integrity": "sha512-A9oDfaGRR89pQho4U7ub338sblq3CTSJh17FhZ8FwrFb3hDxt8Glvh5+gkNaJ3APThyH1ziH7zkYAxwpSZFxWQ==", "license": "Apache-2.0", "dependencies": { "eventemitter3": "^4.0.7", @@ -2090,6 +2085,9 @@ "text-encoding-shim": "^1.0.5", "unmute-ios-audio": "^3.3.0", "web-midi-api": "^2.1.8" + }, + "engines": { + "node": ">=18.18" } }, "node_modules/@csound/browser/node_modules/ramda": { @@ -4255,20 +4253,18 @@ } }, "node_modules/@hlolli/codemirror-lang-csound": { - "version": "1.0.0-alpha10", - "resolved": "https://registry.npmjs.org/@hlolli/codemirror-lang-csound/-/codemirror-lang-csound-1.0.0-alpha10.tgz", - "integrity": "sha512-pcI++pcME4oShgy6teaGzHNWoW9Cp3zdFGH/W5L2EYTkyRA4x+0DK5E7hWRmDDIhdlJE0xqaCrU4GH3oeD1GAA==", + "version": "1.0.0-alpha11", + "resolved": "https://registry.npmjs.org/@hlolli/codemirror-lang-csound/-/codemirror-lang-csound-1.0.0-alpha11.tgz", + "integrity": "sha512-rAxxRgc7sOEQw675Bfdh2N0sai7tImQ3a7J5Tqaux4g3ebRejHHe++YpwtmoHra7mIYC4ZofeUnBJ5eV6Fz15A==", "license": "LGPL-2.0", "dependencies": { - "@codemirror/text": "^0.19.6", - "ramda": "^0.30.1", - "style-mod": "^4.1.2" - }, - "peerDependencies": { - "@codemirror/language": "6.x", - "@codemirror/state": "6.x", - "@codemirror/view": "6.x", - "codemirror": "6.x" + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/language": "^6.12.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.7", + "@lezer/common": "^1.5.2", + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.10" } }, "node_modules/@humanfs/core": { @@ -4534,24 +4530,24 @@ } }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", "license": "MIT" }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -9025,9 +9021,9 @@ } }, "node_modules/codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", diff --git a/package.json b/package.json index 3da309fc..4bb08c7a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,12 @@ "prepare": "husky" }, "dependencies": { - "@csound/browser": "^7.0.0-beta29", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.10.4", + "@codemirror/language": "^6.12.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.8", + "@csound/browser": "^7.0.0-beta32", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@emotion/core": "^11.0.0", @@ -33,11 +38,13 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@hello-pangea/dnd": "^18.0.1", - "@hlolli/codemirror-lang-csound": "^1.0.0-alpha10", + "@hlolli/codemirror-lang-csound": "^1.0.0-alpha11", + "@lezer/common": "^1.5.2", "@mui/icons-material": "^6.2.0", "@mui/material": "^6.2.0", "@reduxjs/toolkit": "^2.5.0", "@vitejs/plugin-react": "^4.3.4", + "codemirror": "^6.0.2", "date-fns": "^4.1.0", "firebase-functions": "^6.4.0", "history": "5.3.0", @@ -101,7 +108,6 @@ "babel-loader": "9.2.1", "babel-plugin-named-asset-import": "^0.3.8", "camelcase": "^8.0.0", - "codemirror": "^6.0.1", "concurrently": "^9.1.0", "cross-env": "^7.0.3", "css-loader": "7.1.2", @@ -149,7 +155,6 @@ "resolutions": { "react-iframe-comm/react": "*", "react-iframe-comm/react-dom": "*", - "@babel/plugin-syntax-class-static-block@^7.0": "7.14.5", - "@hlolli/react-codemirror/codemirror": "^6.0.1" + "@babel/plugin-syntax-class-static-block@^7.0": "7.14.5" } } diff --git a/src/components/editor/utils.test.ts b/src/components/editor/utils.test.ts new file mode 100644 index 00000000..6abae4ba --- /dev/null +++ b/src/components/editor/utils.test.ts @@ -0,0 +1,82 @@ +import { syntaxTree } from "@codemirror/language"; +import { EditorState } from "@codemirror/state"; +import { csoundMode } from "@hlolli/codemirror-lang-csound"; +import { describe, expect, it } from "vitest"; +import { findSurroundingContext } from "./utils"; + +const contextAt = ( + source: string, + fileType: "csd" | "orc" | "sco", + search: string +): string | undefined => { + const state = EditorState.create({ + doc: source, + extensions: [csoundMode({ fileType })] + }); + const position = source.indexOf(search); + if (position < 0) { + throw new Error(`Search string not found in source: ${search}`); + } + const context = findSurroundingContext( + syntaxTree(state).cursorAt(position, 1) + ); + + return context && source.slice(context.from, context.to); +}; + +describe("findSurroundingContext", () => { + it("selects a whole instrument in an orchestra", () => { + const source = [ + "instr 1", + "a1 oscili 0.2, 440", + "out a1", + "endin", + "" + ].join("\n"); + + expect(contextAt(source, "orc", "oscili")).toBe(source.trimEnd()); + }); + + it("selects a whole legacy UDO", () => { + const source = [ + "opcode PassThrough, a, a", + "ain xin", + "xout ain", + "endop", + "" + ].join("\n"); + + expect(contextAt(source, "orc", "xout")).toBe(source.trimEnd()); + }); + + it("selects one top-level orchestra statement", () => { + const source = ["giValue init 1", 'prints "ready"', ""].join("\n"); + + expect(contextAt(source, "orc", "prints")).toBe('prints "ready"\n'); + }); + + it("selects one score statement", () => { + const source = ["f 1 0 1024 10 1", "i 1 0 1", ""].join("\n"); + + expect(contextAt(source, "sco", "i 1")).toBe("i 1 0 1\n"); + }); + + it("selects a whole instrument inside a CSD", () => { + const instrument = [ + "instr 1", + "a1 oscili 0.2, 440", + "out a1", + "endin" + ].join("\n"); + const source = [ + "", + "", + instrument, + "", + "", + "" + ].join("\n"); + + expect(contextAt(source, "csd", "oscili")).toBe(instrument); + }); +}); diff --git a/src/components/editor/utils.ts b/src/components/editor/utils.ts index 70cc049e..891d5f00 100644 --- a/src/components/editor/utils.ts +++ b/src/components/editor/utils.ts @@ -2,7 +2,7 @@ import { curry } from "ramda"; import { syntaxTree } from "@codemirror/language"; import { StateEffect, StateField, Transaction } from "@codemirror/state"; import { Decoration, DecorationSet, EditorView } from "@codemirror/view"; -import { TreeCursor } from "@lezer/common"; +import type { SyntaxNode, TreeCursor } from "@lezer/common"; import type { CsoundObj } from "@comp/csound/types"; const addBlinkSuccessMarks = StateEffect.define(); @@ -46,32 +46,33 @@ export const evalBlinkExtension = StateField.define({ provide: (f) => EditorView.decorations.from(f) }); -const findSurroundingContext = (view: EditorView, tree: TreeCursor) => { - const treeRoot = tree.node; - let maybeContext: any = treeRoot; - let lastContext: any = maybeContext; +const evaluableBlockNames = new Set(["InstrumentDefinition", "UdoDefinition"]); - while (maybeContext) { - if ( - ["InstrumentDeclaration", "UdoDeclaration"].includes( - maybeContext.type.name - ) - ) { - return maybeContext; +export const findSurroundingContext = ( + tree: TreeCursor +): SyntaxNode | undefined => { + let statement: SyntaxNode | undefined; + let node: SyntaxNode | null = tree.node; + + while (node) { + if (evaluableBlockNames.has(node.type.name)) { + return node; } - // if we find ourselves in global scope, check if the user wanted to evaluate a global statement + const parentName = node.parent?.type.name; if ( - maybeContext.type.name === "Program" && - ["OpcodeStatement", "CallbackExpression"].includes( - lastContext.type.name - ) + (node.type.name === "OrcStatement" && + parentName === "OrcStatements") || + (node.type.name === "ScoStatement" && + parentName === "ScoStatements") ) { - return lastContext; + statement = node; } - lastContext = maybeContext; - maybeContext = maybeContext.node.parent; + + node = node.parent; } + + return statement; }; const evalSelection = async ({ @@ -116,19 +117,21 @@ export const editorEvalCode = curry( view.state.selection.main.from !== view.state.selection.main.to; let selection; - let context: { from: number; to: number }; + let context: { from: number; to: number } | undefined; if (userHasSelection && !blockEval) { - selection = view.state.sliceDoc( - view.state.selection.main.from, - view.state.selection.main.to - ); + context = { + from: view.state.selection.main.from, + to: view.state.selection.main.to + }; + selection = view.state.sliceDoc(context.from, context.to); } else if (blockEval) { const treeRoot = syntaxTree(view.state).cursorAt( - view.state.selection.main.head + view.state.selection.main.head, + 1 ); - context = findSurroundingContext(view, treeRoot); + context = findSurroundingContext(treeRoot); if ( typeof context === "object" && @@ -145,7 +148,7 @@ export const editorEvalCode = curry( selection = view.state.sliceDoc(line.from, line.to); } - if (selection) { + if (selection && context) { evalSelection({ csound, documentType, evalString: selection }).then( (result: number) => { if (result === 0) { diff --git a/src/global.d.ts b/src/global.d.ts index 8c6f5954..48e9479c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,7 +4,6 @@ declare module "d3-scale"; declare module "file-saver"; declare module "react-beforeunload"; declare module "react-iframe-comm"; -declare module "@hlolli/codemirror-lang-csound"; declare module "history" { export * from "history";