diff --git a/.vscode/launch.json b/.vscode/launch.json index 9fab1712..a292bca6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,22 @@ "version": "0.2.0", "configurations": [ { - "name": "Chrome", + "name": "Storybook (Chrome)", "type": "chrome", "request": "launch", "url": "http://localhost:6006", "preLaunchTask": "Start Dev Server", "postDebugTask": "Terminate All Tasks", "webRoot": "${workspaceFolder}/packages/components" + }, + { + "name": "Monaco (Chrome)", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5173", + "preLaunchTask": "Start Monaco Dev Server", + "postDebugTask": "Terminate All Tasks", + "webRoot": "${workspaceFolder}/packages/monaco" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4d211459..55a37d02 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -22,6 +22,25 @@ ], "isBackground": true }, + { + "label": "Start Monaco Dev Server", + "type": "shell", + "command": "npm run dev:monaco", + "problemMatcher": [ + { + "owner": "typescript", + "pattern": { + "regexp": "" + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": ".*" + } + } + ], + "isBackground": true + }, { "label": "Terminate All Tasks", "command": "echo ${input:terminate}", diff --git a/build/unpublish-npm.sh b/build/unpublish-npm.sh index 81bfe52b..ec540cc3 100755 --- a/build/unpublish-npm.sh +++ b/build/unpublish-npm.sh @@ -4,3 +4,5 @@ REGISTRY="https://npmjs-registry.ivyteam.ch/" npm unpublish "@axonivy/ui-icons@${1}" --registry $REGISTRY npm unpublish "@axonivy/ui-components@${1}" --registry $REGISTRY +npm unpublish "@axonivy/jsonrpc@${1}" --registry $REGISTRY +npm unpublish "@axonivy/monaco@${1}" --registry $REGISTRY diff --git a/eslint.config.mjs b/eslint.config.mjs index f5b39830..81d3fa93 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,10 +22,10 @@ export default tseslint.config( ignores: ['**/dev-packages/**', '**/.storybook/**'] }, { - name: 'packages/core', - files: ['packages/core/**/*.{js,mjs,cjs,ts,jsx,tsx}'], + name: 'packages/monaco', + files: ['packages/monaco/**/*.{js,mjs,cjs,ts,jsx,tsx}'], rules: { - '@typescript-eslint/no-explicit-any': 'off' + '@typescript-eslint/no-namespace': 'off' } } ); diff --git a/package-lock.json b/package-lock.json index 71cbc6d1..d6ffc870 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,10 @@ "resolved": "packages/jsonrpc", "link": true }, + "node_modules/@axonivy/monaco": { + "resolved": "packages/monaco", + "link": true + }, "node_modules/@axonivy/ui-components": { "resolved": "packages/components", "link": true @@ -576,6 +580,36 @@ "node": ">=6.9.0" } }, + "node_modules/@codingame/monaco-vscode-editor-service-override": { + "version": "1.83.3", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-service-override/-/monaco-vscode-editor-service-override-1.83.3.tgz", + "integrity": "sha512-9j3ixC2KO+U2U4edm27ki17UetdpiQm/nGRAtdLXzUj6fnj34vr4EOyiVaj6/YtCa+qUuEqBrveosvRbUdxJAQ==", + "license": "MIT", + "dependencies": { + "monaco-editor": "0.44.0", + "vscode": "npm:@codingame/monaco-vscode-api@1.83.3" + } + }, + "node_modules/@codingame/monaco-vscode-languages-service-override": { + "version": "1.83.3", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-languages-service-override/-/monaco-vscode-languages-service-override-1.83.3.tgz", + "integrity": "sha512-ECIZbjFnB1gv+KiEhYVH26InXYUhBy1lZ8L8LJqL2fk8M9uni3bnVoV8yfP0OXd0L+tpjsL3hsNXv304a222+g==", + "license": "MIT", + "dependencies": { + "monaco-editor": "0.44.0", + "vscode": "npm:@codingame/monaco-vscode-api@1.83.3" + } + }, + "node_modules/@codingame/monaco-vscode-model-service-override": { + "version": "1.83.3", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-model-service-override/-/monaco-vscode-model-service-override-1.83.3.tgz", + "integrity": "sha512-Bc0LujY0SBMvVJqB1YOVQzJPfApWyhHU79pCQiCPG2HbhdAOw1RDakbZ1iTA2i95q8ksgQTKVzgWLfL1Kj/McA==", + "license": "MIT", + "dependencies": { + "monaco-editor": "0.44.0", + "vscode": "npm:@codingame/monaco-vscode-api@1.83.3" + } + }, "node_modules/@emnapi/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", @@ -1830,6 +1864,32 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -11966,7 +12026,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -12575,6 +12634,52 @@ "node": ">=0.10.0" } }, + "node_modules/monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==" + }, + "node_modules/monaco-editor-workers": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.44.0.tgz", + "integrity": "sha512-rvdO292CMnxs9Y3Hl6nAjVx8d0SjcDgmXmZNVoaOCNJrdnTEEzcWcHJzEQsajTAAq4H2oeBmDZRpDE0US5DhXA==", + "dependencies": { + "monaco-editor": "~0.44.0" + }, + "peerDependencies": { + "monaco-editor": "~0.44.0" + } + }, + "node_modules/monaco-languageclient": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/monaco-languageclient/-/monaco-languageclient-6.6.1.tgz", + "integrity": "sha512-BtuVTfwnFbutgOd4npXj0EXXrp8wl8FENM4ub5pJdV19uK8YwlMtoMcFIGONZp+pxU/gte25k62kAi4r5QsNEw==", + "hasInstallScript": true, + "dependencies": { + "@codingame/monaco-vscode-editor-service-override": "~1.83.3", + "@codingame/monaco-vscode-languages-service-override": "~1.83.3", + "@codingame/monaco-vscode-model-service-override": "~1.83.3", + "monaco-editor": "~0.44.0", + "vscode": "npm:@codingame/monaco-vscode-api@>=1.83.3 <1.84.0", + "vscode-languageclient": "~8.1.0" + }, + "engines": { + "node": ">=16.11.0", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "monaco-editor": "~0.44.0", + "vscode": "npm:@codingame/monaco-vscode-api@>=1.83.3 <1.84.0" + }, + "peerDependenciesMeta": { + "monaco-editor": { + "optional": false + }, + "vscode": { + "optional": false + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -14869,7 +14974,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -15278,6 +15382,12 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -17111,6 +17221,18 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vscode": { + "name": "@codingame/monaco-vscode-api", + "version": "1.83.3", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-api/-/monaco-vscode-api-1.83.3.tgz", + "integrity": "sha512-UhhThNT7mgUrnpLgoW0QiidFjD5vI0ia5uPvw88Z6uj4FngzXG33rqOhA/36xYwkFZKkNqOvSPnCG3zyTd0l2Q==", + "dependencies": { + "monaco-editor": "0.44.0" + }, + "bin": { + "monaco-treemending": "monaco-treemending.js" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -17119,6 +17241,74 @@ "node": ">=14.0.0" } }, + "node_modules/vscode-languageclient": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" + }, + "engines": { + "vscode": "^1.67.0" + } + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageclient/node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient/node_modules/vscode-languageserver-protocol": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "dependencies": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "node_modules/vscode-languageclient/node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", @@ -17512,8 +17702,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.6.1", @@ -17698,6 +17887,30 @@ "devDependencies": { "vitest-websocket-mock": "^0.4.0" } + }, + "packages/monaco": { + "name": "@axonivy/monaco", + "version": "13.1.0-next", + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", + "dependencies": { + "@codingame/monaco-vscode-editor-service-override": "1.83.3", + "@codingame/monaco-vscode-languages-service-override": "1.83.3", + "@codingame/monaco-vscode-model-service-override": "1.83.3", + "@monaco-editor/react": "^4.6.0", + "@react-aria/interactions": "^3.22.2", + "monaco-editor": "0.44.0", + "monaco-editor-workers": "0.44.0", + "monaco-languageclient": "6.6.1", + "vscode-languageserver-protocol": "3.17.5" + }, + "devDependencies": { + "react-dom": "^18.3.1" + }, + "peerDependencies": { + "@axonivy/jsonrpc": "~13.1.0-next", + "@axonivy/ui-components": "~13.1.0-next", + "react": "^18.2 || ^19.0" + } } } } diff --git a/package.json b/package.json index 0588f84c..e6d2af30 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lint:fix": "eslint --fix", "lint:inspect": "eslint --inspect-config", "dev": "npm run dev --workspace=@axonivy/ui-components", + "dev:monaco": "npm run dev --workspace=@axonivy/monaco", "test": "npm run test --workspace=@axonivy/ui-components", "test:ci": "lerna run test:ci", "publish:next": "lerna publish --exact --canary --preid next --pre-dist-tag next --no-git-tag-version --no-push --ignore-scripts --yes" diff --git a/packages/components/package.json b/packages/components/package.json index 71ee9c56..19ce621c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -74,8 +74,6 @@ "build:storybook": "storybook build", "dev": "storybook dev -p 6006 --no-open", "type": "tsc --noEmit --emitDeclarationOnly false", - "lint": "eslint --ext .ts,.tsx ./src", - "lint:fix": "eslint --fix --ext .ts,.tsx ./src", "test": "vitest", "test:ci": "vitest --watch=false" } diff --git a/packages/monaco/index.html b/packages/monaco/index.html new file mode 100644 index 00000000..a4d35a6b --- /dev/null +++ b/packages/monaco/index.html @@ -0,0 +1,21 @@ + + + + + + + + + Monaco Playground + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/packages/monaco/package.json b/packages/monaco/package.json new file mode 100644 index 00000000..aa8a29b1 --- /dev/null +++ b/packages/monaco/package.json @@ -0,0 +1,46 @@ +{ + "name": "@axonivy/monaco", + "version": "13.1.0-next", + "private": false, + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", + "author": "Axon Ivy AG", + "homepage": "https://developer.axonivy.com/", + "repository": { + "type": "git", + "url": "https://github.com/axonivy/ui-components" + }, + "files": [ + "lib", + "src" + ], + "dependencies": { + "@codingame/monaco-vscode-editor-service-override": "1.83.3", + "@codingame/monaco-vscode-languages-service-override": "1.83.3", + "@codingame/monaco-vscode-model-service-override": "1.83.3", + "@monaco-editor/react": "^4.6.0", + "@react-aria/interactions": "^3.22.2", + "monaco-editor": "0.44.0", + "monaco-editor-workers": "0.44.0", + "monaco-languageclient": "6.6.1", + "vscode-languageserver-protocol": "3.17.5" + }, + "peerDependencies": { + "@axonivy/jsonrpc": "~13.1.0-next", + "@axonivy/ui-components": "~13.1.0-next", + "react": "^18.2 || ^19.0" + }, + "devDependencies": { + "react-dom": "^18.3.1" + }, + "type": "module", + "types": "lib/index.d.ts", + "main": "lib/monaco.js", + "module": "lib/monaco.js", + "scripts": { + "clean": "rimraf lib *.tsbuildinfo", + "build": "tsc --build", + "package": "npm run clean && vite build && npm run build", + "dev": "vite", + "type": "tsc --noEmit" + } +} diff --git a/packages/monaco/src/components/CodeEditor.css b/packages/monaco/src/components/CodeEditor.css new file mode 100644 index 00000000..e62fded0 --- /dev/null +++ b/packages/monaco/src/components/CodeEditor.css @@ -0,0 +1,37 @@ +.code-editor { + background: var(--N25); + position: relative; +} +.code-editor .code-input { + border-radius: var(--border-r1); + border: var(--input-border); + font-size: 12px; + line-height: 12px; + color: var(--body); + text-align: start; + padding: var(--input-padding); +} + +.code-editor .code-input:focus-within { + border: var(--activ-border); +} + +.code-editor .monaco-placeholder { + position: absolute; + white-space: nowrap; + top: 12px; + left: 11px; + font-size: 12px; + font-style: italic; + color: var(--N500); + pointer-events: none; + user-select: none; +} + +.code-editor .monaco-placeholder[data-with-line-numbers='true'] { + left: 35px; +} + +.code-editor .header .type { + height: 100%; +} diff --git a/packages/monaco/src/components/CodeEditor.tsx b/packages/monaco/src/components/CodeEditor.tsx new file mode 100644 index 00000000..9dd2866b --- /dev/null +++ b/packages/monaco/src/components/CodeEditor.tsx @@ -0,0 +1,62 @@ +import './CodeEditor.css'; +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { Suspense, lazy, useState } from 'react'; +import { useReadonly } from '@axonivy/ui-components'; +import { MonacoEditorUtil, MONACO_OPTIONS } from '../monaco-editor-util'; + +const Editor = lazy(async () => { + const editor = await import('@monaco-editor/react'); + await MonacoEditorUtil.getInstance(); + return editor; +}); + +export type CodeEditorProps = { + contextPath: string; + value: string; + onChange: (value: string) => void; + language: 'ivyScript' | 'ivyMacro' | (string & {}); + height?: number; + onMountFuncs?: Array<(editor: monaco.editor.IStandaloneCodeEditor) => void>; + options?: monaco.editor.IStandaloneEditorConstructionOptions; +}; + +export const CodeEditor = ({ contextPath, value, onChange, language, onMountFuncs, options, ...props }: CodeEditorProps) => { + const readonly = useReadonly(); + const [showPlaceholder, setShowPlaceholder] = useState(false); + + const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => { + onMountFuncs?.forEach(func => func(editor)); + setShowPlaceholder(editor.getValue() === ''); + }; + + const monacoOptions = { ...(options ?? MONACO_OPTIONS) }; + monacoOptions.readOnly = readonly; + + return ( +
+ Loading Editor...
}> + { + setShowPlaceholder(!code); + onChange(code ?? ''); + }} + onMount={handleEditorDidMount} + {...props} + /> + + + {showPlaceholder && ( +
+ Press CTRL + SPACE for auto-completion +
+ )} + + ); +}; diff --git a/packages/monaco/src/components/ResizableCodeEditor.css b/packages/monaco/src/components/ResizableCodeEditor.css new file mode 100644 index 00000000..da665f9a --- /dev/null +++ b/packages/monaco/src/components/ResizableCodeEditor.css @@ -0,0 +1,16 @@ +.resizable-code-editor { + width: 100%; + position: relative; +} +.resizable-code-editor .resize-line { + cursor: ns-resize; + margin: 0; + position: absolute; + width: 100%; + border: none; + background-color: transparent; + height: 2px; +} +.resizable-code-editor .resize-line:where(:hover, [data-resize-active='true']) { + background-color: var(--body); +} diff --git a/packages/monaco/src/components/ResizableCodeEditor.tsx b/packages/monaco/src/components/ResizableCodeEditor.tsx new file mode 100644 index 00000000..fcef3ed0 --- /dev/null +++ b/packages/monaco/src/components/ResizableCodeEditor.tsx @@ -0,0 +1,31 @@ +import './ResizableCodeEditor.css'; +import { useState } from 'react'; +import type { CodeEditorProps } from './CodeEditor'; +import { CodeEditor } from './CodeEditor'; +import { useMove } from '@react-aria/interactions'; + +export type ResizableCodeEditorProps = Omit & { + initHeight?: number; +}; + +export const ResizableCodeEditor = ({ initHeight, ...props }: ResizableCodeEditorProps) => { + const [height, setHeight] = useState(initHeight ?? 90); + const [resizeActive, setResizeActive] = useState(false); + const { moveProps } = useMove({ + onMoveStart() { + setResizeActive(true); + }, + onMove(e) { + setHeight(y => y + e.deltaY); + }, + onMoveEnd() { + setResizeActive(false); + } + }); + return ( +
+ +
+
+ ); +}; diff --git a/packages/monaco/src/components/SingleLineCodeEditor.tsx b/packages/monaco/src/components/SingleLineCodeEditor.tsx new file mode 100644 index 00000000..1da311d3 --- /dev/null +++ b/packages/monaco/src/components/SingleLineCodeEditor.tsx @@ -0,0 +1,88 @@ +import { useCallback } from 'react'; +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import type { CodeEditorProps } from './CodeEditor'; +import { CodeEditor } from './CodeEditor'; +import { MonacoEditorUtil, SINGLE_LINE_MONACO_OPTIONS } from '../monaco-editor-util'; +import { monacoAutoFocus } from './monaco-utils'; + +export type EditorOptions = { + editorOptions?: { + fixedOverflowWidgets?: boolean; + }; + keyActions?: { + enter?: () => void; + tab?: () => void; + escape?: () => void; + }; + modifyAction?: (value: string) => string; +}; + +export type SingleLineCodeEditorProps = CodeEditorProps & EditorOptions; + +export const SingleLineCodeEditor = ({ onChange, onMountFuncs, editorOptions, keyActions, ...props }: SingleLineCodeEditorProps) => { + const mountFuncs = onMountFuncs ? onMountFuncs : []; + + const singleLineMountFuncs = (editor: monaco.editor.IStandaloneCodeEditor) => { + editor.createContextKey('singleLine', true); + const isSuggestWidgetOpen = (editor: monaco.editor.IStandaloneCodeEditor) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (editor as any)._contentWidgets['editor.widget.suggestWidget']?.widget._widget._state === STATE_OPEN; + const triggerAcceptSuggestion = (editor: monaco.editor.IStandaloneCodeEditor) => + editor.trigger(undefined, 'acceptSelectedSuggestion', undefined); + const STATE_OPEN = 3; + editor.addCommand( + MonacoEditorUtil.KeyCode.Enter, + () => { + if (isSuggestWidgetOpen(editor)) { + triggerAcceptSuggestion(editor); + } else if (keyActions?.enter) { + keyActions.enter(); + } + }, + 'singleLine' + ); + editor.addCommand( + MonacoEditorUtil.KeyCode.Tab, + () => { + if (isSuggestWidgetOpen(editor)) { + triggerAcceptSuggestion(editor); + } else { + if (editor.hasTextFocus() && document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + if (keyActions?.tab) { + keyActions.tab(); + } + } + }, + 'singleLine' + ); + editor.addCommand( + MonacoEditorUtil.KeyCode.Escape, + () => { + if (!isSuggestWidgetOpen(editor) && keyActions?.escape) { + keyActions.escape(); + } + }, + 'singleLine' + ); + }; + + const onCodeChange = useCallback<(code: string) => void>( + code => { + code = code.replace(/[\n\r]/g, ''); + onChange(code); + }, + [onChange] + ); + + return ( + + ); +}; diff --git a/packages/monaco/src/components/monaco-utils.ts b/packages/monaco/src/components/monaco-utils.ts new file mode 100644 index 00000000..538e31b3 --- /dev/null +++ b/packages/monaco/src/components/monaco-utils.ts @@ -0,0 +1,9 @@ +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +export const monacoAutoFocus = (editor: monaco.editor.IStandaloneCodeEditor) => { + const range = editor.getModel()?.getFullModelRange(); + if (range) { + editor.setPosition(range.getEndPosition()); + } + editor.focus(); +}; diff --git a/packages/monaco/src/index.ts b/packages/monaco/src/index.ts new file mode 100644 index 00000000..a748290e --- /dev/null +++ b/packages/monaco/src/index.ts @@ -0,0 +1,11 @@ +export * from './ivy-script-client'; +export * from './monaco-util'; +export * from './monaco-editor-util'; + +export * from './utils/console-util'; +export * from './utils/promises-util'; + +export * from './components/CodeEditor'; +export * from './components/SingleLineCodeEditor'; +export * from './components/ResizableCodeEditor'; +export * from './components/monaco-utils'; diff --git a/packages/monaco/src/ivy-macro-language.ts b/packages/monaco/src/ivy-macro-language.ts new file mode 100644 index 00000000..cbc1550e --- /dev/null +++ b/packages/monaco/src/ivy-macro-language.ts @@ -0,0 +1,162 @@ +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +export const ivyMacroLang: monaco.languages.IMonarchLanguage = { + defaultToken: '', + tokenPostfix: '.ivyscript', + keywords: [ + 'continue', + 'for', + 'new', + 'boolean', + 'if', + 'IF', + 'break', + 'double', + 'byte', + 'else', + 'import', + 'instanceof', + 'catch', + 'int', + 'short', + 'try', + 'char', + 'finally', + 'long', + 'float', + 'while', + 'true', + 'false', + 'is', + 'initialized', + 'as' + ], + operators: [ + '=', + '>', + '<', + '!', + '?', + ':', + '==', + '<=', + '>=', + '!=', + '<>', + '&&', + '||', + '++', + '--', + '+', + '-', + '*', + '/', + '&', + '|', + '^', + '%', + '**', + '+=', + '-=', + '*=', + '/=', + '%=', + '**=' + ], + symbols: /[=>/, 'tag'], + [/[<>](?!@symbols)/, '@brackets'], + [ + /@symbols/, + { + cases: { + '@operators': 'delimiter', + '@default': '' + } + } + ], + [/(@digits)[eE]([-+]?(@digits))?[fFdD]?/, 'number.float'], + [/(@digits)\.(@digits)([eE][-+]?(@digits))?[fFdD]?/, 'number.float'], + [/0[xX](@hexdigits)[Ll]?/, 'number.hex'], + [/0(@octaldigits)[Ll]?/, 'number.octal'], + [/0[bB](@binarydigits)[Ll]?/, 'number.binary'], + [/(@digits)[fFdD]/, 'number.float'], + [/(@digits)[lL]?/, 'number'], + [/[;,.]/, 'delimiter'], + [/"([^"\\]|\\.)*$/, 'string.invalid'], + [/"/, 'string', '@string'], + [/'[^\\']'/, 'string'], + [/(')(@escapes)(')/, ['string', 'string.escape', 'string']], + [/'/, 'string.invalid'] + ], + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'] + ], + comment: [ + [/[^/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[/*]/, 'comment'] + ], + string: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'] + ] + } +}; + +export const ivyMacroConf: monaco.languages.LanguageConfiguration = { + wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+[{\]}\\|;:'",.<>/?\s]+)/g, + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '<%', close: '%>' } + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '<', close: '>' } + ], + folding: { + markers: { + start: new RegExp('^\\s*//\\s*(?:(?:#?region\\b)|(?:))') + } + } +}; diff --git a/packages/monaco/src/ivy-script-client.ts b/packages/monaco/src/ivy-script-client.ts new file mode 100644 index 00000000..1cbc3109 --- /dev/null +++ b/packages/monaco/src/ivy-script-client.ts @@ -0,0 +1,19 @@ +import { urlBuilder, type Connection } from '@axonivy/jsonrpc'; + +export namespace IvyScriptLanguage { + export function webSocketUrl(url: string | URL) { + return urlBuilder(url, 'ivy-script-lsp'); + } + + export async function startClient(connection: Connection, isMonacoReady: Promise) { + await isMonacoReady; + const monacoLanguageClient = await import('monaco-languageclient'); + const client = new monacoLanguageClient.MonacoLanguageClient({ + name: 'IvyScript Language Client', + clientOptions: { documentSelector: [{ language: 'ivyScript' }, { language: 'ivyMacro' }] }, + connectionProvider: { get: async () => connection } + }); + client.start(); + return client; + } +} diff --git a/packages/monaco/src/ivy-script-language.ts b/packages/monaco/src/ivy-script-language.ts new file mode 100644 index 00000000..d922f03f --- /dev/null +++ b/packages/monaco/src/ivy-script-language.ts @@ -0,0 +1,160 @@ +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +export const ivyScriptLang: monaco.languages.IMonarchLanguage = { + defaultToken: '', + tokenPostfix: '.ivyscript', + keywords: [ + 'continue', + 'for', + 'new', + 'boolean', + 'if', + 'IF', + 'break', + 'double', + 'byte', + 'else', + 'import', + 'instanceof', + 'catch', + 'int', + 'short', + 'try', + 'char', + 'finally', + 'long', + 'float', + 'while', + 'true', + 'false', + 'is', + 'initialized', + 'as' + ], + operators: [ + '=', + '>', + '<', + '!', + '?', + ':', + '==', + '<=', + '>=', + '!=', + '<>', + '&&', + '||', + '++', + '--', + '+', + '-', + '*', + '/', + '&', + '|', + '^', + '%', + '**', + '+=', + '-=', + '*=', + '/=', + '%=', + '**=' + ], + symbols: /[=>](?!@symbols)/, '@brackets'], + [ + /@symbols/, + { + cases: { + '@operators': 'delimiter', + '@default': '' + } + } + ], + [/(@digits)[eE]([-+]?(@digits))?[fFdD]?/, 'number.float'], + [/(@digits)\.(@digits)([eE][-+]?(@digits))?[fFdD]?/, 'number.float'], + [/0[xX](@hexdigits)[Ll]?/, 'number.hex'], + [/0(@octaldigits)[Ll]?/, 'number.octal'], + [/0[bB](@binarydigits)[Ll]?/, 'number.binary'], + [/(@digits)[fFdD]/, 'number.float'], + [/(@digits)[lL]?/, 'number'], + [/[;,.]/, 'delimiter'], + [/"([^"\\]|\\.)*$/, 'string.invalid'], + [/"/, 'string', '@string'], + [/'[^\\']'/, 'string'], + [/(')(@escapes)(')/, ['string', 'string.escape', 'string']], + [/'/, 'string.invalid'] + ], + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'] + ], + comment: [ + [/[^/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[/*]/, 'comment'] + ], + string: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'] + ] + } +}; + +export const ivyScriptConf: monaco.languages.LanguageConfiguration = { + wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+[{\]}\\|;:'",.<>/?\s]+)/g, + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" } + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '<', close: '>' } + ], + folding: { + markers: { + start: new RegExp('^\\s*//\\s*(?:(?:#?region\\b)|(?:))') + } + } +}; diff --git a/packages/monaco/src/monaco-editor-util.ts b/packages/monaco/src/monaco-editor-util.ts new file mode 100644 index 00000000..b50fce7b --- /dev/null +++ b/packages/monaco/src/monaco-editor-util.ts @@ -0,0 +1,189 @@ +import type { editor } from 'monaco-editor/esm/vs/editor/editor.api'; +import { ivyMacroConf, ivyMacroLang } from './ivy-macro-language'; +import { ivyScriptConf, ivyScriptLang } from './ivy-script-language'; + +import type * as monacoEditorReact from '@monaco-editor/react'; +import { Deferred } from './utils/promises-util'; +import { MonacoUtil, type MonacoEditorApi, type MonacoLanguageClientConfig, type MonacoWorkerConfig } from './monaco-util'; +import { ConsoleTimer } from './utils/console-util'; +export type MonacoEditorReactApi = typeof monacoEditorReact; + +type ThemeMode = 'light' | 'dark'; + +export const MONACO_OPTIONS: editor.IStandaloneEditorConstructionOptions = { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + overviewRulerBorder: false, + overviewRulerLanes: 1, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + folding: false, + renderLineHighlight: 'none', + fontFamily: + "'Droid Sans Mono', 'monospace', monospace, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: 12, + tabSize: 2, + renderWhitespace: 'all', + fixedOverflowWidgets: true, + scrollbar: { + useShadows: false + } +}; + +export const MAXIMIZED_MONACO_OPTIONS: editor.IStandaloneEditorConstructionOptions = { + ...MONACO_OPTIONS, + lineNumbers: 'on', + folding: true, + showFoldingControls: 'always' +}; + +export const SINGLE_LINE_MONACO_OPTIONS: editor.IStandaloneEditorConstructionOptions = { + ...MONACO_OPTIONS, + overviewRulerLanes: 0, + overviewRulerBorder: false, + hideCursorInOverviewRuler: true, + scrollBeyondLastColumn: 0, + scrollbar: { + horizontal: 'hidden', + vertical: 'hidden', + alwaysConsumeMouseWheel: false + }, + find: { + addExtraSpaceOnTop: false, + autoFindInSelection: 'never', + seedSearchStringFromSelection: 'never' + }, + links: false, + renderLineHighlight: 'none', + contextmenu: false +}; + +export namespace MonacoEditorUtil { + export const DEFAULT_THEME_NAME = 'axon-input'; + + export function themeData(theme?: ThemeMode): editor.IStandaloneThemeData { + if (theme === 'dark') { + return { + base: 'vs-dark', + colors: { + 'editor.foreground': '#FFFFFF', + 'editorCursor.foreground': '#FFFFFF', + 'editor.background': '#333333' + }, + inherit: true, + rules: [] + }; + } + return { + base: 'vs', + colors: { + 'editor.foreground': '#202020', + 'editorCursor.foreground': '#202020', + 'editor.background': '#fafafa' + }, + inherit: true, + rules: [] + }; + } + + const instance: Deferred = new Deferred(); + export async function getInstance(): Promise { + return instance.promise; + } + + let configureCalled = false; + export async function configureInstance(configuration?: MonacoConfiguration): Promise { + if (configureCalled) { + console.warn( + 'MonacoEditorUtil.configureInstance should only be called once. The caller will receive the first, configured instance. If you want to configure additional instances, call "configureMonacoReactEditor" instead.' + ); + } else { + configureCalled = true; + configureMonacoReactEditor(configuration).then(instance.resolve).catch(instance.reject); + } + return instance.promise; + } + + // We want to avoid an import to import { KeyCode } from 'monaco-editor/esm/vs/editor/editor.api'. + // So we replicate the necessary Key codes here since they are very stable. + export enum KeyCode { + Tab = 2, + Enter = 3, + Escape = 9, + F2 = 60 + } + + let monacoEditorReactApiPromise: Promise; + export async function monacoEditorReactApi(): Promise { + if (!monacoEditorReactApiPromise) { + monacoEditorReactApiPromise = import('@monaco-editor/react'); + } + return monacoEditorReactApiPromise; + } + + export async function setTheme(theme?: ThemeMode): Promise { + const monacoApi = await getInstance(); + monacoApi.editor.defineTheme(MonacoEditorUtil.DEFAULT_THEME_NAME, MonacoEditorUtil.themeData(theme)); + } +} + +// from @monaco-editor/loader +export interface MonacoLoaderConfig { + paths?: { + vs?: string; + }; + 'vs/nls'?: { + availableLanguages?: object; + }; + monaco?: MonacoEditorApi; +} + +export interface MonacoConfiguration { + loader?: MonacoLoaderConfig; + worker?: MonacoWorkerConfig; + languageClient?: MonacoLanguageClientConfig; + theme?: ThemeMode; + debug?: boolean; +} + +export async function configureMonacoReactEditor(configuration?: MonacoConfiguration): Promise { + const timer = new ConsoleTimer(configuration?.debug, 'Configure Monaco React Editor'); + timer.start(); + + timer.step('Start loading Monaco Editor React API...'); + const reactEditorApi = await MonacoEditorUtil.monacoEditorReactApi(); + + timer.step('Start loading Monaco Editor API...'); + const monaco = configuration?.loader?.monaco ?? (await MonacoUtil.monacoEditorApi()); + const reactEditorLoader = reactEditorApi.loader; + reactEditorLoader.config({ ...configuration?.loader, monaco }); + + // configure Monaco environment, must be called after configuring monaco + timer.step('Start configuring Monaco Environment...'); + await MonacoUtil.configureEnvironment({ + languageClient: configuration?.languageClient, + worker: configuration?.worker, + debug: configuration?.debug + }); + + timer.step('Initialize Monaco React Editor...'); + const monacoApi = await reactEditorLoader.init(); + monacoApi.languages.register({ + id: 'ivyScript', + extensions: ['.ivyScript', '.ivyScript'], + aliases: ['IvyScript', 'ivyScript'] + }); + monacoApi.languages.register({ + id: 'ivyMacro', + extensions: ['.ivyMacro', '.ivyMacro'], + aliases: [] + }); + monacoApi.languages.setLanguageConfiguration('ivyScript', ivyScriptConf); + monacoApi.languages.setMonarchTokensProvider('ivyScript', ivyScriptLang); + monacoApi.languages.setLanguageConfiguration('ivyMacro', ivyMacroConf); + monacoApi.languages.setMonarchTokensProvider('ivyMacro', ivyMacroLang); + monacoApi.editor.defineTheme(MonacoEditorUtil.DEFAULT_THEME_NAME, MonacoEditorUtil.themeData(configuration?.theme)); + timer.end(); + return monacoApi; +} diff --git a/packages/monaco/src/monaco-util.ts b/packages/monaco/src/monaco-util.ts new file mode 100644 index 00000000..9ac5d8c5 --- /dev/null +++ b/packages/monaco/src/monaco-util.ts @@ -0,0 +1,162 @@ +import { Deferred } from './utils/promises-util'; + +import type * as monacoLanguageClient from 'monaco-languageclient'; +export type MonacoLanguageClient = typeof monacoLanguageClient; + +import type * as monacoEditorWorkers from 'monaco-editor-workers'; +export type MonacoEditorWorkers = typeof monacoEditorWorkers; + +import type * as monacoEditorApi from 'monaco-editor'; +import { ConsoleTimer, logIf } from './utils/console-util'; +export type MonacoEditorApi = typeof monacoEditorApi; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WorkerConstructor = (new (...args: any) => Worker) | (new (...args: any) => Promise); + +// from monaco-editor-workers +export interface MonacoWorkerConfig { + workerPath?: string; + basePath?: string; + useModuleWorker?: boolean; + + // extension + workerType?: 'typescript' | 'javascript' | 'html' | 'handlebars' | 'razor' | 'css' | 'scss' | 'less' | 'json' | 'editor'; + workerConstructor?: WorkerConstructor; + skip?: boolean; + debug?: boolean; +} + +export interface MonacoLanguageClientConfig extends monacoLanguageClient.InitializeServiceConfig { + initializationDelay?: number; + initializationMaxTries?: number; + + skip?: boolean; + debug?: boolean; +} + +export namespace MonacoUtil { + let monacoLanguageClientPromise: Promise; + export async function monacoLanguageClient(): Promise { + if (!monacoLanguageClientPromise) { + monacoLanguageClientPromise = import('monaco-languageclient'); + } + return monacoLanguageClientPromise; + } + + let monacoEditorWorkersPromise: Promise; + export async function monacoEditorWorkers(): Promise { + if (!monacoEditorWorkersPromise) { + monacoEditorWorkersPromise = import('monaco-editor-workers'); + } + return monacoEditorWorkersPromise; + } + + let monacoEditorApiPromise: Promise; + export async function monacoEditorApi(): Promise { + if (!monacoEditorApiPromise) { + monacoEditorApiPromise = import('monaco-editor'); + } + return monacoEditorApiPromise; + } + + /** + * Imports all services and initializes the VS Code extension API for the language client. + * If complete, the vscodeApiInitialised will be set on the MonacoEnvironment. + * You can query this flag through the 'monacoInitialized' function. + */ + export async function configureLanguageClient(config?: MonacoLanguageClientConfig): Promise { + if (config?.skip) { + logIf(config.debug, 'Skip Monaco Language Client Configuration.'); + return; + } + const timer = new ConsoleTimer(config?.debug, 'Configure Language Client'); + timer.start(); + timer.step('Start initializing Services and VS Code Extension API...'); + const languageClient = await monacoLanguageClient(); + await languageClient.initServices(config); + timer.step('Waiting for VS Code API to be initialized...'); + await monacoInitialized(config?.initializationDelay, config?.initializationMaxTries); + timer.end(); + } + + /** + * Ensures that we have the necessary MonacoEnvironment.getWorker function available. + */ + export async function configureWorkers(config?: MonacoWorkerConfig): Promise { + if (config?.skip) { + logIf(config.debug, 'Skip Monaco Worker Configuration.'); + return; + } + const timer = new ConsoleTimer(config?.debug, 'Configure Monaco Workers').start(); + + // default behavior for MonacoEnvironment.getWorker + timer.step('Start configuring MonacoEnvironment.getWorker...'); + const monacoEditorWorker = await monacoEditorWorkers(); + monacoEditorWorker.buildWorkerDefinition( + config?.workerPath ?? '../../../node_modules/monaco-editor-workers/dist/workers', + config?.basePath ?? import.meta.url, + config?.useModuleWorker ?? false + ); + const defaultGetWorker = self.MonacoEnvironment?.getWorker; + + // overridden behavior for MonacoEnvironment.getWorker if an explicit worker constructor is given + if (config?.workerConstructor) { + timer.step('Override MonacoEnvironment.getWorker with given WorkerConstructor...'); + const WorkerConstructor = config.workerConstructor; + + self.MonacoEnvironment = { + ...self.MonacoEnvironment, + async getWorker(id, label) { + try { + timer.log('[MonacoEnvironment] Create Worker...'); + const worker = await new WorkerConstructor(id, label); + timer.log('[MonacoEnvironment] Success.'); + return worker; + } catch (error) { + console.error(error); + timer.log('[MonacoEnvironment] Default to fallback worker...'); + if (defaultGetWorker) { + const worker = await defaultGetWorker(id, config.workerType ?? label); + timer.log('[MonacoEnvironment] Success.'); + return worker; + } + throw error; + } + } + }; + } + timer.end(); + } + + export async function configureEnvironment(config?: { + worker?: MonacoWorkerConfig; + languageClient?: MonacoLanguageClientConfig; + debug?: boolean; + }): Promise { + await Promise.all([ + MonacoUtil.configureWorkers({ ...config?.worker, debug: config?.worker?.debug ?? config?.debug }), + MonacoUtil.configureLanguageClient({ ...config?.languageClient, debug: config?.languageClient?.debug ?? config?.debug }) + ]); + } + + export async function monacoInitialized(delay: number = 100, maxTries: number = 30): Promise { + const deferred = new Deferred(); + let tries = 0; + const initializationCheck = async () => { + try { + tries += 1; + if ((await monacoLanguageClient()).wasVscodeApiInitialized()) { + deferred.resolve(); + } else if (tries < maxTries) { + setTimeout(initializationCheck, delay); + } else { + deferred.reject(new Error('Monaco initialization timed out.')); + } + } catch (error) { + deferred.reject(error); + } + }; + initializationCheck(); + return deferred.promise; + } +} diff --git a/packages/monaco/src/playground/EditorChooser.tsx b/packages/monaco/src/playground/EditorChooser.tsx new file mode 100644 index 00000000..33e4b883 --- /dev/null +++ b/packages/monaco/src/playground/EditorChooser.tsx @@ -0,0 +1,33 @@ +import { BasicField, BasicSelect, Flex, Separator } from '@axonivy/ui-components'; +import { useState } from 'react'; +import { IvyScriptEditor } from './IvyScriptEditor'; +import { IvyScriptArea } from './IvyScriptArea'; +import { IvyScriptInput } from './IvyScriptInput'; +import { IvyMacroArea } from './IvyMacroArea'; +import { IvyMacroInput } from './IvyMacroInput'; + +const editors = [ + { value: 'IvyScriptEditor', label: 'IvyScript Editor' }, + { value: 'IvyScriptArea', label: 'IvyScript Area' }, + { value: 'IvyScriptInput', label: 'IvyScript Input' }, + { value: 'IvyMacroArea', label: 'IvyMacro Area' }, + { value: 'IvyMacroInput', label: 'IvyMacro Input' } +]; + +export const EditorChooser = () => { + const [editor, setEditor] = useState('IvyScriptEditor'); + + return ( + + + + + + {editor === 'IvyScriptEditor' && } + {editor === 'IvyScriptArea' && } + {editor === 'IvyScriptInput' && } + {editor === 'IvyMacroArea' && } + {editor === 'IvyMacroInput' && } + + ); +}; diff --git a/packages/monaco/src/playground/IvyMacroArea.tsx b/packages/monaco/src/playground/IvyMacroArea.tsx new file mode 100644 index 00000000..96411f87 --- /dev/null +++ b/packages/monaco/src/playground/IvyMacroArea.tsx @@ -0,0 +1,17 @@ +import { BasicField } from '@axonivy/ui-components'; +import { useState } from 'react'; +import { ResizableCodeEditor } from '../components/ResizableCodeEditor'; + +export const IvyMacroArea = () => { + const [value, setValue] = useState(''); + return ( + + + + ); +}; diff --git a/packages/monaco/src/playground/IvyMacroInput.tsx b/packages/monaco/src/playground/IvyMacroInput.tsx new file mode 100644 index 00000000..19a3554a --- /dev/null +++ b/packages/monaco/src/playground/IvyMacroInput.tsx @@ -0,0 +1,20 @@ +import { BasicField } from '@axonivy/ui-components'; +import { useState } from 'react'; +import { SingleLineCodeEditor } from '../components/SingleLineCodeEditor'; + +export const IvyMacroInput = () => { + const [value, setValue] = useState(''); + return ( + + + + ); +}; diff --git a/packages/monaco/src/playground/IvyScriptArea.tsx b/packages/monaco/src/playground/IvyScriptArea.tsx new file mode 100644 index 00000000..3a22f5a0 --- /dev/null +++ b/packages/monaco/src/playground/IvyScriptArea.tsx @@ -0,0 +1,17 @@ +import { BasicField } from '@axonivy/ui-components'; +import { useState } from 'react'; +import { ResizableCodeEditor } from '../components/ResizableCodeEditor'; + +export const IvyScriptArea = () => { + const [value, setValue] = useState(''); + return ( + + + + ); +}; diff --git a/packages/monaco/src/playground/IvyScriptEditor.tsx b/packages/monaco/src/playground/IvyScriptEditor.tsx new file mode 100644 index 00000000..a235ebe6 --- /dev/null +++ b/packages/monaco/src/playground/IvyScriptEditor.tsx @@ -0,0 +1,20 @@ +import { BasicField } from '@axonivy/ui-components'; +import { useState } from 'react'; +import { MAXIMIZED_MONACO_OPTIONS } from '../monaco-editor-util'; +import { CodeEditor } from '../components/CodeEditor'; + +export const IvyScriptEditor = () => { + const [value, setValue] = useState(''); + return ( + + + + ); +}; diff --git a/packages/monaco/src/playground/IvyScriptInput.tsx b/packages/monaco/src/playground/IvyScriptInput.tsx new file mode 100644 index 00000000..942faba4 --- /dev/null +++ b/packages/monaco/src/playground/IvyScriptInput.tsx @@ -0,0 +1,20 @@ +import { BasicField } from '@axonivy/ui-components'; +import { useState } from 'react'; +import { SingleLineCodeEditor } from '../components/SingleLineCodeEditor'; + +export const IvyScriptInput = () => { + const [value, setValue] = useState(''); + return ( + + + + ); +}; diff --git a/packages/monaco/src/playground/index.css b/packages/monaco/src/playground/index.css new file mode 100644 index 00000000..53c2ee8d --- /dev/null +++ b/packages/monaco/src/playground/index.css @@ -0,0 +1,4 @@ +@layer icons; + +@import '@axonivy/ui-icons/src-gen/ivy-icons.css' layer(icons); +@import '@axonivy/ui-components/lib/style.css'; diff --git a/packages/monaco/src/playground/index.tsx b/packages/monaco/src/playground/index.tsx new file mode 100644 index 00000000..fdae6bd9 --- /dev/null +++ b/packages/monaco/src/playground/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { MonacoEditorUtil } from '../monaco-editor-util'; +import './index.css'; +import { webSocketConnection, type Connection } from '@axonivy/jsonrpc'; +import type { MonacoLanguageClient } from 'monaco-languageclient'; +import { IvyScriptLanguage } from '../ivy-script-client'; +import { URLParams } from './url-helper'; +import { EditorChooser } from './EditorChooser'; +import { Button, Flex, ThemeProvider, useTheme } from '@axonivy/ui-components'; +import { IvyIcons } from '@axonivy/ui-icons'; + +export async function start(): Promise { + const server = URLParams.webSocketBase(); + const rootElement = document.getElementById('root'); + if (!rootElement) { + throw new Error('root element not found'); + } + const root = createRoot(rootElement); + + const worker = await import('monaco-editor/esm/vs/editor/editor.worker?worker'); + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const instance = MonacoEditorUtil.configureInstance({ theme: systemTheme, debug: true, worker: { workerConstructor: worker.default } }); + const initializeScript = async (connection: Connection) => { + return await IvyScriptLanguage.startClient(connection, instance); + }; + const reconnectScript = async (connection: Connection, oldClient: MonacoLanguageClient) => { + try { + await oldClient.stop(0); + } catch (error) { + console.warn(error); + } + return initializeScript(connection); + }; + webSocketConnection(IvyScriptLanguage.webSocketUrl(server)).listen({ + onConnection: initializeScript, + onReconnect: reconnectScript, + logger: console + }); + + root.render( + + + + + + ); +} + +start(); + +const Playground = () => { + const { theme, setTheme } = useTheme(); + const changeTheme = () => { + const newTheme = theme === 'dark' ? 'light' : 'dark'; + setTheme(newTheme); + MonacoEditorUtil.setTheme(newTheme); + }; + return ( +
+ +

Monaco Playground

+
+ ); +}; diff --git a/packages/monaco/src/playground/url-helper.ts b/packages/monaco/src/playground/url-helper.ts new file mode 100644 index 00000000..8f2f97f9 --- /dev/null +++ b/packages/monaco/src/playground/url-helper.ts @@ -0,0 +1,57 @@ +export namespace URLParams { + export function parameter(key: string): string | undefined { + const param = new URLSearchParams(window.location.search).get(key); + return param !== null ? decodeURIComponent(param) : undefined; + } + + export function app(): string { + return parameter('app') ?? ''; + } + + export function pmv(): string { + return parameter('pmv') ?? ''; + } + + export function pid(): string { + return parameter('pid') ?? ''; + } + + export function webSocketBase(): string { + return `${isSecureConnection() ? 'wss' : 'ws'}://${server()}`; + } + + export function themeMode(): 'dark' | 'light' { + const theme = parameter('theme'); + if (theme === 'dark') { + return theme; + } + if (theme === 'light') { + return 'light'; + } + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + const isSecureConnection = () => { + const secureParam = parameter('secure'); + if (secureParam === 'true') { + return true; + } + if (secureParam === 'false') { + return false; + } + return window.location.protocol === 'https:'; + }; + + const server = () => { + return parameter('server') ?? basePath(); + }; + + const basePath = () => { + const protocol = window.location.protocol; + const href = window.location.href; + if (href.includes('/process-inscription')) { + return href.substring(protocol.length + 2, href.indexOf('/process-inscription')); + } + return 'localhost:8081'; + }; +} diff --git a/packages/monaco/src/utils/console-util.ts b/packages/monaco/src/utils/console-util.ts new file mode 100644 index 00000000..18725d58 --- /dev/null +++ b/packages/monaco/src/utils/console-util.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function logIf(condition?: boolean, message?: any, ...optionalParams: any[]): void { + if (condition) { + console.log(message, ...optionalParams); + } +} + +export function timeIf(condition?: boolean, label?: string): void { + if (condition) { + console.time(label); + } +} + +export function timeEndIf(condition?: boolean, label?: string): void { + if (condition) { + console.timeEnd(label); + } +} + +export function timeLogIf(condition?: boolean, label?: string, ...data: any[]): void { + if (condition) { + console.timeLog(label, ...data); + } +} + +export class ConsoleTimer { + constructor(protected condition: boolean | undefined, protected label: string) {} + + log(message?: any, ...optionalParams: any[]) { + logIf(this.condition, message, ...optionalParams); + } + + start(): ConsoleTimer { + timeIf(this.condition, this.label); + return this; + } + + step(...data: any[]): ConsoleTimer { + timeLogIf(this.condition, this.label, ...data); + return this; + } + + end(): ConsoleTimer { + timeEndIf(this.condition, this.label); + return this; + } +} diff --git a/packages/monaco/src/utils/promises-util.ts b/packages/monaco/src/utils/promises-util.ts new file mode 100644 index 00000000..45183ed6 --- /dev/null +++ b/packages/monaco/src/utils/promises-util.ts @@ -0,0 +1,36 @@ +// ***************************************************************************** +// Copyright (C) 2017 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +// from https://github.com/eclipse-theia/theia/blob/4d7f225e8c87c51152ed605b3f47460f0163a408/packages/core/src/common/promise-util.ts#L4 +export class Deferred { + state: 'resolved' | 'rejected' | 'unresolved' = 'unresolved'; + resolve: (value: T | PromiseLike) => void; + reject: (err?: unknown) => void; + + promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }).then( + res => (this.setState('resolved'), res), + err => (this.setState('rejected'), Promise.reject(err)) + ); + + protected setState(state: 'resolved' | 'rejected'): void { + if (this.state === 'unresolved') { + this.state = state; + } + } +} diff --git a/packages/monaco/tsconfig.json b/packages/monaco/tsconfig.json new file mode 100644 index 00000000..0449094f --- /dev/null +++ b/packages/monaco/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../config/tsconfig.package.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/packages/monaco/tsconfig.production.json b/packages/monaco/tsconfig.production.json new file mode 100644 index 00000000..747e26a3 --- /dev/null +++ b/packages/monaco/tsconfig.production.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/playground"] +} diff --git a/packages/monaco/vite.config.ts b/packages/monaco/vite.config.ts new file mode 100644 index 00000000..1045c053 --- /dev/null +++ b/packages/monaco/vite.config.ts @@ -0,0 +1,43 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import { visualizer } from 'rollup-plugin-visualizer'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + plugins: [visualizer(), dts({ tsconfigPath: './tsconfig.production.json' })], + resolve: { + alias: { + '@': resolve(__dirname, './src') + } + }, + build: { + outDir: 'lib', + sourcemap: true, + chunkSizeWarningLimit: 5000, + rollupOptions: { + output: { + manualChunks(id: string) { + if (id.includes('monaco-languageclient') || id.includes('vscode')) { + return 'monaco-chunk'; + } + } + }, + external: [ + '@axonivy/ui-icons', + '@axonivy/ui-components', + '@axonivy/jsonrpc', + 'react', + 'react/jsx-runtime', + 'react-dom' + // 'monaco-languageclient', + // 'monaco-editor', + // 'vscode' + ] + }, + lib: { + entry: resolve(__dirname, 'src/index.ts'), + fileName: 'monaco', + formats: ['es'] + } + } +}); diff --git a/ui-components.code-workspace b/ui-components.code-workspace index 5d7f6f1f..cb804e1f 100644 --- a/ui-components.code-workspace +++ b/ui-components.code-workspace @@ -18,7 +18,11 @@ { "name": "package/jsonrpc", "path": "./packages/jsonrpc" - } + }, + { + "name": "package/monaco", + "path": "./packages/monaco" + }, ], "settings": { "task.autoDetect": "off",