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",