diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5b54e20..4dad0b1 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -1,6 +1,18 @@ import { resolve } from 'path' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import react from '@vitejs/plugin-react' +// https://github.com/vdesjs/vite-plugin-monaco-editor/issues/21#issuecomment-1827562674 +import monacoEditorPluginModule from 'vite-plugin-monaco-editor' +const isObjectWithDefaultFunction = ( + module: unknown +): module is { default: typeof monacoEditorPluginModule } => + module != null && + typeof module === 'object' && + 'default' in module && + typeof module.default === 'function' +const monacoEditorPlugin = isObjectWithDefaultFunction(monacoEditorPluginModule) + ? monacoEditorPluginModule.default + : monacoEditorPluginModule export default defineConfig({ main: { @@ -15,6 +27,18 @@ export default defineConfig({ '@renderer': resolve('src/renderer/src') } }, - plugins: [react()] + plugins: [ + react(), + monacoEditorPlugin({ + languageWorkers: ['editorWorkerService', 'typescript', 'json'], + customDistPath: () => 'out/renderer', + customWorkers: [ + { + label: 'yaml', + entry: 'monaco-yaml/yaml.worker' + } + ] + }) + ] } }) diff --git a/package.json b/package.json index 5f4df9d..96bb9cd 100644 --- a/package.json +++ b/package.json @@ -31,15 +31,6 @@ "@electron-toolkit/eslint-config-ts": "^2.0.0", "@electron-toolkit/tsconfig": "^1.0.1", "@nextui-org/react": "^2.4.6", - "dayjs": "^1.11.12", - "framer-motion": "^11.3.21", - "next-themes": "^0.3.0", - "pubsub-js": "^1.9.4", - "react-icons": "^5.2.1", - "react-monaco-editor": "^0.56.0", - "react-router-dom": "^6.26.0", - "react-virtuoso": "^4.9.0", - "swr": "^2.2.5", "@types/node": "^22.1.0", "@types/pubsub-js": "^1.8.6", "@types/react": "^18.3.3", @@ -48,19 +39,33 @@ "@vitejs/plugin-react": "^4.3.1", "adm-zip": "^0.5.15", "autoprefixer": "^10.4.20", + "dayjs": "^1.11.12", "electron": "^31.3.1", "electron-builder": "^25.0.3", "electron-vite": "^2.3.0", "eslint": "^8.57.0", "eslint-plugin-react": "^7.35.0", + "framer-motion": "^11.3.21", + "meta-json-schema": "^1.18.6", + "monaco-yaml": "^5.2.2", + "nanoid": "^5.0.7", + "next-themes": "^0.3.0", "postcss": "^8.4.41", "prettier": "^3.3.3", + "pubsub-js": "^1.9.4", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.2.1", + "react-monaco-editor": "^0.56.0", + "react-router-dom": "^6.26.0", + "react-virtuoso": "^4.9.0", + "swr": "^2.2.5", "tailwindcss": "^3.4.7", "tar": "^7.4.3", "tsx": "^4.16.5", + "types-pac": "^1.0.2", "typescript": "^5.5.4", - "vite": "^5.3.5" + "vite": "^5.3.5", + "vite-plugin-monaco-editor": "^1.1.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0ef65f..0f494ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,15 @@ importers: framer-motion: specifier: ^11.3.21 version: 11.3.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + meta-json-schema: + specifier: ^1.18.6 + version: 1.18.6 + monaco-yaml: + specifier: ^5.2.2 + version: 5.2.2(monaco-editor@0.44.0) + nanoid: + specifier: ^5.0.7 + version: 5.0.7 next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -126,12 +135,18 @@ importers: tsx: specifier: ^4.16.5 version: 4.16.5 + types-pac: + specifier: ^1.0.2 + version: 1.0.2 typescript: specifier: ^5.5.4 version: 5.5.4 vite: specifier: ^5.3.5 version: 5.3.5(@types/node@22.1.0) + vite-plugin-monaco-editor: + specifier: ^1.1.0 + version: 1.1.0(monaco-editor@0.44.0) packages: @@ -3112,6 +3127,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -3231,6 +3249,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meta-json-schema@1.18.6: + resolution: {integrity: sha512-HMDu+1lcVsgEsa4kPlyo2ZNgOEbaQJhOzU4ll4qJahwx5TIw8NkcbXgxnKEW6NieqL4AOWWtoNgw3EKeuHg4+A==} + micromatch@4.0.7: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} @@ -3331,6 +3352,25 @@ packages: monaco-editor@0.44.0: resolution: {integrity: sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==} + monaco-languageserver-types@0.4.0: + resolution: {integrity: sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==} + + monaco-marker-data-provider@1.2.3: + resolution: {integrity: sha512-BOiQs9UNEwVrF1rwYI32HUP8D7JTuHlJRlykx83e4+jfh1ceBWIBfB5ENDVSFUz651d95kxjKj36vV2JO3zr9w==} + + monaco-types@0.1.0: + resolution: {integrity: sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==} + + monaco-worker-manager@2.0.1: + resolution: {integrity: sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==} + peerDependencies: + monaco-editor: '>=0.30.0' + + monaco-yaml@5.2.2: + resolution: {integrity: sha512-NWO/UhtJATlIsqwWPzK7YfbcIvPo3riFGsUkaGxNJoGiNPOvHD8vZ83ecqMQGkHPOpgHtSbe94uokE1AJvpbyQ==} + peerDependencies: + monaco-editor: '>=0.36' + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -3345,6 +3385,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.0.7: + resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3468,6 +3513,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3570,6 +3618,11 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + prettier@3.3.3: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} @@ -4099,6 +4152,9 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} + types-pac@1.0.2: + resolution: {integrity: sha512-9zOLBtvgzEesEgpkJPrrh+uGTzSdNcYA+gk1jv3+14ytTsuxnunBko98OgbCCgUfcMzraEi4h+3zPrZaPoEsQg==} + typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} @@ -4196,6 +4252,11 @@ packages: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} + vite-plugin-monaco-editor@1.1.0: + resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==} + peerDependencies: + monaco-editor: '>=0.33.0' + vite@5.3.5: resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4224,6 +4285,22 @@ packages: terser: optional: true + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -8536,6 +8613,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -8660,6 +8739,8 @@ snapshots: merge2@1.4.1: {} + meta-json-schema@1.18.6: {} + micromatch@4.0.7: dependencies: braces: 3.0.3 @@ -8745,6 +8826,37 @@ snapshots: monaco-editor@0.44.0: {} + monaco-languageserver-types@0.4.0: + dependencies: + monaco-types: 0.1.0 + vscode-languageserver-protocol: 3.17.5 + vscode-uri: 3.0.8 + + monaco-marker-data-provider@1.2.3: + dependencies: + monaco-types: 0.1.0 + + monaco-types@0.1.0: {} + + monaco-worker-manager@2.0.1(monaco-editor@0.44.0): + dependencies: + monaco-editor: 0.44.0 + + monaco-yaml@5.2.2(monaco-editor@0.44.0): + dependencies: + jsonc-parser: 3.3.1 + monaco-editor: 0.44.0 + monaco-languageserver-types: 0.4.0 + monaco-marker-data-provider: 1.2.3 + monaco-types: 0.1.0 + monaco-worker-manager: 2.0.1(monaco-editor@0.44.0) + path-browserify: 1.0.1 + prettier: 2.8.8 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + yaml: 2.5.0 + ms@2.1.2: {} ms@2.1.3: {} @@ -8757,6 +8869,8 @@ snapshots: nanoid@3.3.7: {} + nanoid@5.0.7: {} + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -8896,6 +9010,8 @@ snapshots: dependencies: callsites: 3.1.0 + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -8974,6 +9090,8 @@ snapshots: dependencies: fast-diff: 1.3.0 + prettier@2.8.8: {} + prettier@3.3.3: {} process-nextick-args@2.0.1: {} @@ -9612,6 +9730,8 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 + types-pac@1.0.2: {} + typescript@5.5.4: {} unbox-primitive@1.0.2: @@ -9694,6 +9814,10 @@ snapshots: extsprintf: 1.4.1 optional: true + vite-plugin-monaco-editor@1.1.0(monaco-editor@0.44.0): + dependencies: + monaco-editor: 0.44.0 + vite@5.3.5(@types/node@22.1.0): dependencies: esbuild: 0.21.5 @@ -9703,6 +9827,19 @@ snapshots: '@types/node': 22.1.0 fsevents: 2.3.3 + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-uri@3.0.8: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4 diff --git a/src/renderer/src/components/base/base-editor.tsx b/src/renderer/src/components/base/base-editor.tsx new file mode 100644 index 0000000..d45d2da --- /dev/null +++ b/src/renderer/src/components/base/base-editor.tsx @@ -0,0 +1,100 @@ +import { useEffect, useRef } from 'react' +import * as monaco from 'monaco-editor' +import MonacoEditor from 'react-monaco-editor' +import { configureMonacoYaml } from 'monaco-yaml' +import metaSchema from 'meta-json-schema/schemas/meta-json-schema.json' +import pac from 'types-pac/pac.d.ts?raw' +import { useTheme } from 'next-themes' +import { nanoid } from 'nanoid' +import React from 'react' +type Language = 'yaml' | 'javascript' | 'json' + +interface Props { + value: string + readOnly?: boolean + language: Language + onChange?: (value: string) => void +} + +let initialized = false +const monacoInitialization = (): void => { + if (initialized) return + + // configure yaml worker + configureMonacoYaml(monaco, { + validate: true, + enableSchemaRequest: true, + schemas: [ + { + uri: 'http://example.com/meta-json-schema.json', + fileMatch: ['**/*.clash.yaml'], + // @ts-ignore // type JSONSchema7 + schema: metaSchema + } + ] + }) + // configure PAC definition + monaco.languages.typescript.javascriptDefaults.addExtraLib(pac, 'pac.d.ts') + initialized = true +} + +export const BaseEditor: React.FC = (props) => { + const { theme } = useTheme() + + const { value, readOnly = false, language, onChange } = props + + const editorRef = useRef() + + const editorWillMount = (): void => { + monacoInitialization() + } + + const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor): void => { + editorRef.current = editor + + const uri = monaco.Uri.parse(`${nanoid()}.${language === 'yaml' ? 'clash' : ''}.${language}`) + const model = monaco.editor.createModel(value, language, uri) + editorRef.current?.setModel(model) + } + + useEffect(() => { + window.electron.ipcRenderer.on('resize', () => { + editorRef.current?.layout() + }) + + return (): void => { + window.electron.ipcRenderer.removeAllListeners('resize') + editorRef.current?.dispose() + editorRef.current = undefined + } + }, []) + + return ( + = 1500 // 超过一定宽度显示minimap滚动条 + }, + mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例 + readOnly: readOnly, // 只读模式 + renderValidationDecorations: 'on', // 只读模式下显示校验信息 + quickSuggestions: { + strings: true, // 字符串类型的建议 + comments: true, // 注释类型的建议 + other: true // 其他类型的建议 + }, + fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji", "Noto Color Emoji"`, + fontLigatures: true, // 连字符 + smoothScrolling: true // 平滑滚动 + }} + editorWillMount={editorWillMount} + editorDidMount={editorDidMount} + onChange={onChange} + /> + ) +} diff --git a/src/renderer/src/components/profiles/edit-file-modal.tsx b/src/renderer/src/components/profiles/edit-file-modal.tsx index 406b8fc..a643f1f 100644 --- a/src/renderer/src/components/profiles/edit-file-modal.tsx +++ b/src/renderer/src/components/profiles/edit-file-modal.tsx @@ -1,7 +1,6 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react' import React, { useEffect, useState } from 'react' -import MonacoEditor, { monaco } from 'react-monaco-editor' -import { useTheme } from 'next-themes' +import { BaseEditor } from '../base/base-editor' import { getProfileStr, setProfileStr } from '@renderer/utils/ipc' interface Props { id: string @@ -10,18 +9,6 @@ interface Props { const EditFileModal: React.FC = (props) => { const { id, onClose } = props const [currData, setCurrData] = useState('') - const { theme } = useTheme() - - const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor): void => { - window.electron.ipcRenderer.on('resize', () => { - editor.layout() - }) - } - - const editorWillUnmount = (editor: monaco.editor.IStandaloneCodeEditor): void => { - window.electron.ipcRenderer.removeAllListeners('resize') - editor.dispose() - } const getContent = async (): Promise => { setCurrData(await getProfileStr(id)) @@ -43,22 +30,10 @@ const EditFileModal: React.FC = (props) => { 编辑订阅 - setCurrData(value)} /> diff --git a/src/renderer/src/components/sider/config-viewer.tsx b/src/renderer/src/components/sider/config-viewer.tsx index 3c2ba6a..bdb2c6f 100644 --- a/src/renderer/src/components/sider/config-viewer.tsx +++ b/src/renderer/src/components/sider/config-viewer.tsx @@ -1,7 +1,6 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react' import React, { useEffect, useState } from 'react' -import MonacoEditor, { monaco } from 'react-monaco-editor' -import { useTheme } from 'next-themes' +import { BaseEditor } from '../base/base-editor' import { getRuntimeConfigStr } from '@renderer/utils/ipc' interface Props { onClose: () => void @@ -9,18 +8,6 @@ interface Props { const ConfigViewer: React.FC = (props) => { const { onClose } = props const [currData, setCurrData] = useState('') - const { theme } = useTheme() - - const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor): void => { - window.electron.ipcRenderer.on('resize', () => { - editor.layout() - }) - } - - const editorWillUnmount = (editor: monaco.editor.IStandaloneCodeEditor): void => { - window.electron.ipcRenderer.removeAllListeners('resize') - editor.dispose() - } const getContent = async (): Promise => { setCurrData(await getRuntimeConfigStr()) @@ -42,24 +29,7 @@ const ConfigViewer: React.FC = (props) => { 当前运行时配置 - +