From 07b8841f3b29003b08494772a63ed7165dcc5786 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 6 Aug 2026 07:51:01 +1000 Subject: [PATCH 1/3] add code editor from source-pane --- package-lock.json | 319 ++++++++++++++++++++++++++++++ package.json | 14 +- src/index.ts | 1 + src/lib/code-editor/CodeEditor.ts | 161 +++++++++++++++ src/lib/code-editor/index.ts | 1 + test/unit/codeEditor.test.js | 190 ++++++++++++++++++ test/unit/index.test.ts | 3 +- 7 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 src/lib/code-editor/CodeEditor.ts create mode 100644 src/lib/code-editor/index.ts create mode 100644 test/unit/codeEditor.test.js diff --git a/package-lock.json b/package-lock.json index a3132364a..db05cc923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,22 @@ "license": "MIT", "dependencies": { "@awesome.me/webawesome": "^3.9.0", + "@codemirror/commands": "^6.10.0", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.3.1", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/language": "^6.12.3", + "@codemirror/legacy-modes": "^6.5.3", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.43.0", "@lit/context": "^1.1.6", "@lit/task": "^1.0.3", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", + "@uiw/codemirror-theme-vscode": "^4.25.10", "escape-html": "^1.0.3", "lit": "^3.3.3", "mime-types": "^3.0.2", @@ -1941,6 +1953,169 @@ "specificity": "bin/cli.js" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", + "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.12", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz", + "integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.2.tgz", + "integrity": "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz", + "integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.8.tgz", + "integrity": "sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.0.tgz", @@ -3057,6 +3232,95 @@ "integrity": "sha512-6Ae3AfG45MOqF9Qde+1YxIxKf/GUsRmcriV7iCb/HaR+Mk1YDFYP3RINDRMI8z8YMWW3BnSthOujX9RoOex/Kw==", "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.6.tgz", + "integrity": "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz", + "integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lit-labs/ssr": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-4.1.0.tgz", @@ -3139,6 +3403,12 @@ "@lit/reactive-element": "^1.0.0 || ^2.0.0" } }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", @@ -5246,6 +5516,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-theme-vscode": { + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.25.11.tgz", + "integrity": "sha512-fTQ5676L9iV3Y6UyDBDd8Wt9Gewh55aC5rqnCtIpwyVFb6zJEHJScI4g43lzAD2IKhY7HlozB1ucrS3UqBFNEQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.11" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.11.tgz", + "integrity": "sha512-SBNCOgRsCtewGNocRbmjbCkltGXlFcPJsvhxQ351VynQjnWUiPbUrFcEU/haQ3HanROdAAjWXZJPk5bMBxl2jw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", @@ -6729,6 +7030,12 @@ "node": ">=0.10.0" } }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", @@ -13348,6 +13655,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -14645,6 +14958,12 @@ "integrity": "sha512-XadVyw0xE+oZ5FGApXsdswv96rOhStzKqL53uSe5UaTadABGkWIg1+DTx8kiZ/VqTZTBneoL0l65RcPe4W3ecw==", "license": "MIT" }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index ed207d4fd..a07f67ef6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-ui", - "version": "3.1.3-15", + "version": "3.1.3-16", "description": "UI library for Solid applications", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", @@ -93,7 +93,19 @@ "homepage": "https://github.com/SolidOS/solid-ui", "dependencies": { "@awesome.me/webawesome": "^3.9.0", + "@codemirror/commands": "^6.10.0", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-markdown": "^6.3.1", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/language": "^6.12.3", + "@codemirror/legacy-modes": "^6.5.3", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.43.0", "@lit/context": "^1.1.6", + "@uiw/codemirror-theme-vscode": "^4.25.10", "@lit/task": "^1.0.3", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", diff --git a/src/index.ts b/src/index.ts index 639490d84..10b6aac46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,3 +67,4 @@ export * from './lib/auth' export * from './lib/components' export * from './lib/dialogs' export * from './lib/file-explorer/context' +export * from './lib/code-editor' diff --git a/src/lib/code-editor/CodeEditor.ts b/src/lib/code-editor/CodeEditor.ts new file mode 100644 index 000000000..8d1cab6a7 --- /dev/null +++ b/src/lib/code-editor/CodeEditor.ts @@ -0,0 +1,161 @@ +import type { ThemeMode } from 'pane-registry' +import { Compartment, EditorState } from '@codemirror/state' +import { EditorView, drawSelection, keymap, lineNumbers } from '@codemirror/view' +import { defaultHighlightStyle, syntaxHighlighting, StreamLanguage } from '@codemirror/language' +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands' +import { css } from '@codemirror/lang-css' +import { html } from '@codemirror/lang-html' +import { javascript } from '@codemirror/lang-javascript' +import { json } from '@codemirror/lang-json' +import { markdown } from '@codemirror/lang-markdown' +import { xml } from '@codemirror/lang-xml' +import { vscodeDark, vscodeLight } from '@uiw/codemirror-theme-vscode' +import { turtle } from '@codemirror/legacy-modes/mode/turtle' +import { sparql } from '@codemirror/legacy-modes/mode/sparql' +import { ntriples } from '@codemirror/legacy-modes/mode/ntriples' + +export class CodeEditor { + private _view: EditorView | null = null + private _languageCompartment: Compartment | null = null + private _editableCompartment: Compartment | null = null + private _onDirtyChange?: (dirty: boolean) => void + private _isDirty = false + + async initialize (container: HTMLElement, initialDoc = '', contentType: string = 'text/turtle', theme: ThemeMode = 'dark', onDirtyChange?: (dirty: boolean) => void) { + if (this._view) { + this._view.destroy() + } + + this._languageCompartment = new Compartment() + this._editableCompartment = new Compartment() + this._onDirtyChange = onDirtyChange + this._isDirty = false + const languageExtension = await this._getLanguageExtension(contentType) + + const state = EditorState.create({ + doc: initialDoc, + extensions: [ + theme === 'dark' ? vscodeDark : vscodeLight, + EditorView.theme({ + '&': { + minHeight: '6lh' + } + }), + this._languageCompartment.of(languageExtension), + this._editableCompartment.of(EditorView.editable.of(true)), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + lineNumbers(), + history(), + drawSelection(), + EditorView.lineWrapping, + EditorView.updateListener.of((update) => { + if (update.docChanged && !this._isDirty) { + this._isDirty = true + this._onDirtyChange?.(true) + } + }), + keymap.of([ + ...defaultKeymap, + ...historyKeymap + ]) + ] + }) + + this._view = new EditorView({ + state, + parent: container + }) + } + + destroy () { + this._view?.destroy() + this._view = null + this._isDirty = false + } + + getValue (): string { + return this._view ? this._view.state.doc.toString() : '' + } + + replaceContent (text: string) { + if (!this._view) return + const current = this._view.state.doc.toString() + if (current === text) return + this._view.dispatch({ + changes: { from: 0, to: this._view.state.doc.length, insert: text } + }) + } + + resetDirtyState () { + this._isDirty = false + } + + setReadOnly (readOnly: boolean) { + if (!this._view) return + const editableCompartment = this._editableCompartment + if (!editableCompartment) return + this._view.dispatch({ + effects: editableCompartment.reconfigure(EditorView.editable.of(!readOnly)) + }) + } + + focusEditor () { + this._view?.focus() + } + + async setLanguage (contentType: string) { + if (!this._view) return + const languageCompartment = this._languageCompartment + if (!languageCompartment) return + + const extension = await this._getLanguageExtension(contentType) + this._view.dispatch({ + effects: languageCompartment.reconfigure(extension) + }) + } + + private async _getLanguageExtension (contentType: string) { + switch (contentType) { + case 'text/turtle': + case 'text/n3': + return StreamLanguage.define(turtle) + + case 'application/sparql-update': + case 'application/sparql-query': + return StreamLanguage.define(sparql) + + case 'application/nquads': + case 'application/n-quads': + case 'application/n-triples': + return StreamLanguage.define(ntriples) + + case 'application/json': + case 'application/ld+json': + return json() + + case 'text/html': + case 'application/xhtml+xml': + return html() + + case 'text/markdown': + case 'text/x-markdown': + case 'text/md': + return markdown() + + case 'application/rdf+xml': + case 'application/xml': + return xml() + + case 'text/css': + return css() + + case 'text/javascript': + case 'application/javascript': + case 'application/ecmascript': + return javascript() + + default: + return [] + } + } +} diff --git a/src/lib/code-editor/index.ts b/src/lib/code-editor/index.ts new file mode 100644 index 000000000..51fade8bb --- /dev/null +++ b/src/lib/code-editor/index.ts @@ -0,0 +1 @@ +export * from './CodeEditor' diff --git a/test/unit/codeEditor.test.js b/test/unit/codeEditor.test.js new file mode 100644 index 000000000..872978396 --- /dev/null +++ b/test/unit/codeEditor.test.js @@ -0,0 +1,190 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@codemirror/state', () => { + class Compartment { + of (value) { + return { type: 'compartment-of', value } + } + + reconfigure (value) { + return { type: 'reconfigure', value } + } + } + + const EditorState = { + create: vi.fn((config) => ({ ...config })) + } + + return { Compartment, EditorState } +}) + +const { createdViews, updateListenerOf } = vi.hoisted(() => ({ + createdViews: [], + updateListenerOf: vi.fn((listener) => ({ type: 'updateListener', listener })) +})) + +vi.mock('@codemirror/view', () => { + class EditorView { + constructor ({ state, parent }) { + this.state = state + this.parent = parent + this.dom = globalThis.document.createElement('div') + this.dispatch = vi.fn((transaction) => { + this.lastTransaction = transaction + if (transaction.changes) { + this.state.doc = transaction.changes.insert + } + }) + this.focus = vi.fn() + this.destroy = vi.fn() + createdViews.push(this) + if (parent) { + parent.appendChild(this.dom) + } + } + } + + EditorView.editable = { + of: vi.fn((value) => ({ type: 'editable', value })) + } + EditorView.theme = vi.fn((spec, options) => ({ type: 'theme', spec, options })) + EditorView.lineWrapping = { type: 'lineWrapping' } + EditorView.updateListener = { + of: updateListenerOf + } + + return { + EditorView, + drawSelection: vi.fn(() => ({ type: 'drawSelection' })), + keymap: { + of: vi.fn((value) => ({ type: 'keymap', value })) + }, + lineNumbers: vi.fn(() => ({ type: 'lineNumbers' })) + } +}) + +vi.mock('@codemirror/language', () => ({ + defaultHighlightStyle: { name: 'defaultHighlightStyle' }, + syntaxHighlighting: vi.fn((style, options) => ({ type: 'syntaxHighlighting', style, options })), + HighlightStyle: { + define: vi.fn(() => ({ type: 'highlightStyle' })) + }, + StreamLanguage: class { + static define (mode) { + return { type: 'stream-language', mode } + } + } +})) + +vi.mock('@codemirror/commands', () => ({ + defaultKeymap: [{ key: 'default' }], + history: vi.fn(() => ({ type: 'history' })), + historyKeymap: [{ key: 'history' }] +})) + +vi.mock('@codemirror/lang-css', () => ({ + css: vi.fn(() => ({ type: 'css' })) +})) + +vi.mock('@codemirror/lang-html', () => ({ + html: vi.fn(() => ({ type: 'html' })) +})) + +vi.mock('@codemirror/lang-javascript', () => ({ + javascript: vi.fn(() => ({ type: 'javascript' })) +})) + +vi.mock('@codemirror/lang-json', () => ({ + json: vi.fn(() => ({ type: 'json' })) +})) + +vi.mock('@codemirror/lang-markdown', () => ({ + markdown: vi.fn(() => ({ type: 'markdown' })) +})) + +vi.mock('@codemirror/lang-xml', () => ({ + xml: vi.fn(() => ({ type: 'xml' })) +})) + +vi.mock('@codemirror/legacy-modes/mode/turtle', () => ({ + turtle: { name: 'turtle' } +})) + +vi.mock('@codemirror/legacy-modes/mode/sparql', () => ({ + sparql: { name: 'sparql' } +})) + +vi.mock('@codemirror/legacy-modes/mode/ntriples', () => ({ + ntriples: { name: 'ntriples' } +})) + +vi.mock('@uiw/codemirror-theme-vscode', () => ({ + vscodeDark: [{ type: 'darkTheme' }], + vscodeLight: [{ type: 'lightTheme' }] +})) + +import { CodeEditor } from '../../src/lib/code-editor' + +describe('CodeEditor', () => { + beforeEach(() => { + createdViews.length = 0 + document.body.innerHTML = '' + vi.clearAllMocks() + }) + + it('initializes with the provided document text', async () => { + const editor = new CodeEditor() + const container = document.createElement('div') + document.body.appendChild(container) + + await editor.initialize(container, 'hello world', 'text/plain') + + expect(editor.getValue()).toBe('hello world') + expect(createdViews).toHaveLength(1) + expect(createdViews[0].state.doc).toBe('hello world') + expect(createdViews[0].parent).toBe(container) + }) + + it('replaces content and toggles read only state', async () => { + const editor = new CodeEditor() + const container = document.createElement('div') + document.body.appendChild(container) + + await editor.initialize(container, 'first', 'text/plain') + const view = createdViews[0] + + editor.replaceContent('second') + expect(editor.getValue()).toBe('second') + expect(view.dispatch).toHaveBeenCalledWith({ + changes: { from: 0, to: 5, insert: 'second' } + }) + + editor.setReadOnly(true) + expect(view.dispatch).toHaveBeenLastCalledWith({ + effects: { type: 'reconfigure', value: { type: 'editable', value: false } } + }) + + editor.focusEditor() + expect(view.focus).toHaveBeenCalled() + }) + + it('reports dirty state once when the document first changes', async () => { + const editor = new CodeEditor() + const container = document.createElement('div') + const onDirtyChange = vi.fn() + + document.body.appendChild(container) + + await editor.initialize(container, 'first', 'text/plain', 'dark', onDirtyChange) + + const updateListener = updateListenerOf.mock.calls[0]?.[0] + expect(updateListener).toBeInstanceOf(Function) + + updateListener({ docChanged: true }) + updateListener({ docChanged: true }) + updateListener({ docChanged: false }) + + expect(onDirtyChange).toHaveBeenCalledTimes(1) + expect(onDirtyChange).toHaveBeenCalledWith(true) + }) +}) diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index e800108a7..89a6d9818 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -9,6 +9,7 @@ describe('Index', () => { expect(Object.keys(Index).sort()).toEqual([ 'Account', 'CloseDialogEvent', + 'CodeEditor', 'DEFAULT_AUTH_CONTEXT', 'DEFAULT_DIALOG_CONTEXT', 'DEFAULT_SIGNUP_URL', @@ -27,7 +28,7 @@ describe('Index', () => { 'create', 'createTypes', 'customElement', - 'dialogContext', + 'dialogContext', 'dom', 'fileExplorerContext', 'generateId', From 8db1df34a9f70eece0db7cb30456540bc956997c Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 9 Aug 2026 12:27:57 +1000 Subject: [PATCH 2/3] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8368a17a3..537d739b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-ui", - "version": "3.1.3-16", + "version": "3.1.3-17", "description": "UI library for Solid applications", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", From de918925b25f341837adb2c3555c127a1dad4982 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 9 Aug 2026 02:29:21 +0000 Subject: [PATCH 3/3] chore: update solidos dependencies (dev: solid-logic@4.0.8-3 pane-registry@3.1.2-2) (latest: rdflib@2.4.0) --- package-lock.json | 38 +++++++++++++++++++------------------- package.json | 4 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53b2b6849..81f5eeb77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-ui", - "version": "3.1.3-16", + "version": "3.1.3-17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-ui", - "version": "3.1.3-16", + "version": "3.1.3-17", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -6572,9 +6572,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6635,9 +6635,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -6655,11 +6655,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -7475,9 +7475,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.402", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", - "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", + "version": "1.5.403", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz", + "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==", "dev": true, "license": "ISC" }, @@ -8127,9 +8127,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-18.2.2.tgz", - "integrity": "sha512-gOO0lIqwEjZ750kv9/SptCWArUoAZXJoBr0vYWTO2dCBxctHUXlBIigiC8xuxxr/NKqgIT6Ehz1xRcilj8a5cA==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-18.3.0.tgz", + "integrity": "sha512-cPVguuDe6DrIPb/qUXHf8P89MaVTUmiYWwpt5gX5AILsvRIiZAxMFXcFR6QHYBksqKJpjfUBlL/RleCJUWcD7w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 537d739b8..038d6b2f2 100644 --- a/package.json +++ b/package.json @@ -96,19 +96,19 @@ "@codemirror/commands": "^6.10.0", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-markdown": "^6.3.1", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.3.1", "@codemirror/lang-xml": "^6.1.0", "@codemirror/language": "^6.12.3", "@codemirror/legacy-modes": "^6.5.3", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.43.0", "@lit/context": "^1.1.6", - "@uiw/codemirror-theme-vscode": "^4.25.10", "@lit/task": "^1.0.3", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", + "@uiw/codemirror-theme-vscode": "^4.25.10", "escape-html": "^1.0.3", "lit": "^3.3.3", "mime-types": "^3.0.2",