diff --git a/plugins/ide-open/.gitignore b/plugins/ide-open/.gitignore new file mode 100644 index 00000000..6d6c01d8 --- /dev/null +++ b/plugins/ide-open/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Test reference project +test/ diff --git a/plugins/ide-open/CHANGELOG.md b/plugins/ide-open/CHANGELOG.md new file mode 100644 index 00000000..faffa7ef --- /dev/null +++ b/plugins/ide-open/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## 1.0.0 + +### Added +- 从 uTools VscodeOpen 插件迁移至 ZTools 平台 +- 支持 VSCode 系编辑器(VS Code、Cursor、VSCodium、Qoder)最近项目读取(SQLite/JSON) +- 支持 JetBrains 系编辑器(IntelliJ IDEA、PyCharm、WebStorm、GoLand)最近项目读取(XML) +- 动态 Feature 注册,输入别名即可进入对应编辑器项目列表 +- setSubInput 搜索 + 键盘导航(↑↓ 选择、Enter 打开、Ctrl+D 删除) +- IDE 配置页面,支持快速填入预设路径 +- 暗色模式支持 + +### Fixed +- 修复命令注入漏洞,添加 escapeShellArg 对参数进行 shell 转义 +- JetBrains 版本目录排序,优先匹配最新版本 +- 添加 safeDecodeURIComponent 防止畸形 URI 导致崩溃 +- 使用 fileURLToPath 替代手动路径转换,修复 Windows 平台路径问题 +- 移除 Settings 页面冗余的 doRegister 逻辑 diff --git a/plugins/ide-open/README.md b/plugins/ide-open/README.md new file mode 100644 index 00000000..22934525 --- /dev/null +++ b/plugins/ide-open/README.md @@ -0,0 +1,73 @@ +# ide-open + +> 统一管理 VSCode 系及 JetBrains 系编辑器的最近项目 — ZTools 插件 + +## 功能 + +- 从编辑器的本地数据库/配置文件中读取最近项目列表 +- 搜索并快速打开项目,支持键盘导航(↑↓ 选择、Enter 打开、Ctrl+D 删除) +- 支持动态注册快捷指令,输入别名即可进入对应编辑器的项目列表 + +### 支持的编辑器 + +| 编辑器 | 数据源格式 | 启动命令 | +|---|---|---| +| VS Code | SQLite (.vscdb) / JSON | `code` | +| Cursor | SQLite (.vscdb) | `cursor` | +| VSCodium | SQLite (.vscdb) | `codium` | +| Qoder | SQLite (.vscdb) | `qoder` | +| IntelliJ IDEA | XML (recentProjects.xml) | `idea` | +| PyCharm | XML | `pycharm` | +| WebStorm | XML | `webstorm` | +| GoLand | XML | `goland` | + +## 项目结构 + +``` +├── public/ +│ ├── preload/ +│ │ ├── package.json # preload 依赖(sql.js) +│ │ ├── services.js # Node.js 能力:SQLite/JSON/XML 读取、项目打开、删除 +│ │ └── sql-wasm.wasm # sql.js 的 wasm 文件 +│ ├── plugin.json # 插件配置 +│ └── logo.png +├── src/ +│ ├── App.tsx # 入口:onPluginEnter 路由分发 +│ ├── store.ts # 渲染层数据封装 +│ ├── env.d.ts # 类型声明 +│ ├── Settings/ # IDE 配置页面(增删改、快速填入预设) +│ └── ProjectList/ # 项目列表页面(setSubInput 搜索 + 键盘导航) +├── vite.config.js # base: './' +└── tsconfig.json +``` + +## 开发 + +```bash +# 安装依赖(前端 + preload) +npm install +cd public/preload && npm install && cd .. + +# 启动开发服务器 +npm run dev + +# 构建 +npm run build +``` + +在 ZTools 中配置插件开发地址指向 `http://localhost:5173` 即可热加载调试。 + +## 使用 + +1. 在 ZTools 主搜索框输入 `ideopen` 进入设置页 +2. 点击「+ 新增配置」,可点击「快速填入」按钮自动填充预设路径 +3. 保存后,ZTools 主搜索框输入配置的别名(如 `vsc`、`cursor`)即可进入项目列表 +4. 在项目列表中搜索并打开项目 + +## 技术栈 + +React 19 + Vite + TypeScript + sql.js + +## License + +MIT diff --git a/plugins/ide-open/index.html b/plugins/ide-open/index.html new file mode 100644 index 00000000..84b7eb68 --- /dev/null +++ b/plugins/ide-open/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+ + + diff --git a/plugins/ide-open/package-lock.json b/plugins/ide-open/package-lock.json new file mode 100644 index 00000000..65036e58 --- /dev/null +++ b/plugins/ide-open/package-lock.json @@ -0,0 +1,929 @@ +{ + "name": "ide-open", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ide-open", + "version": "1.0.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.1", + "@ztools-center/ztools-api-types": "^1.0.1", + "typescript": "^5.3.0", + "vite": "^6.0.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@ztools-center/ztools-api-types": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.381", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + } + } +} diff --git a/plugins/ide-open/package.json b/plugins/ide-open/package.json new file mode 100644 index 00000000..7a0d22f2 --- /dev/null +++ b/plugins/ide-open/package.json @@ -0,0 +1,24 @@ +{ + "name": "ide-open", + "version": "1.0.0", + "description": "插件 — 统一管理 VSCode 系及 JetBrains 系编辑器的最近项目。", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "changelog": "node scripts/gen-changelog.mjs", + "release": "npm run changelog && git add CHANGELOG.md && git commit -m \"chore: update CHANGELOG\" && ztools publish" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.1", + "@ztools-center/ztools-api-types": "^1.0.1", + "typescript": "^5.3.0", + "vite": "^6.0.11" + } +} diff --git a/plugins/ide-open/public/logo.png b/plugins/ide-open/public/logo.png new file mode 100644 index 00000000..ae084fc5 Binary files /dev/null and b/plugins/ide-open/public/logo.png differ diff --git a/plugins/ide-open/public/plugin.json b/plugins/ide-open/public/plugin.json new file mode 100644 index 00000000..f8433224 --- /dev/null +++ b/plugins/ide-open/public/plugin.json @@ -0,0 +1,29 @@ +{ + "$schema": "node_modules/@ztools-center/ztools-api-types/resource/ztools.schema.json", + "name": "ide-open", + "title": "ide-open", + "description": "统一管理 VSCode 系及 JetBrains 系编辑器的最近项目", + "author": "svenzhao", + "version": "1.0.0", + "main": "index.html", + "preload": "preload/services.js", + "logo": "logo.png", + "development": { + "main": "http://localhost:5173" + }, + "features": [ + { + "code": "ideopen", + "explain": "打开 ideOpen 设置,配置编辑器并注册快捷指令", + "icon": "logo.png", + "cmds": [ + "ideopen" + ] + } + ], + "platform": [ + "darwin", + "win32", + "linux" + ] +} diff --git a/plugins/ide-open/public/preload/package-lock.json b/plugins/ide-open/public/preload/package-lock.json new file mode 100644 index 00000000..a96bcff2 --- /dev/null +++ b/plugins/ide-open/public/preload/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "preload", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "sql.js": "^1.14.1" + } + }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + } + } +} diff --git a/plugins/ide-open/public/preload/package.json b/plugins/ide-open/public/preload/package.json new file mode 100644 index 00000000..ac041d2f --- /dev/null +++ b/plugins/ide-open/public/preload/package.json @@ -0,0 +1,6 @@ +{ + "type": "commonjs", + "dependencies": { + "sql.js": "^1.14.1" + } +} diff --git a/plugins/ide-open/public/preload/services.js b/plugins/ide-open/public/preload/services.js new file mode 100644 index 00000000..bbae817d --- /dev/null +++ b/plugins/ide-open/public/preload/services.js @@ -0,0 +1,570 @@ +const fs = require('node:fs') +const path = require('node:path') +const { exec } = require('node:child_process') +const { fileURLToPath } = require('node:url') + +// ─── 文件日志(开发调试) ── + +let _logFile = '' +function getLogFile() { + if (_logFile) return _logFile + try { + const base = window.ztools.getPath('userData') + _logFile = path.join(base, 'ideopen-debug.log') + } catch { + _logFile = path.join(__dirname, 'ideopen-debug.log') + } + return _logFile +} + +function debugLog(message, data) { + let isDev = false + try { isDev = window.ztools.isDev() } catch {} + if (!isDev) return + + const now = new Date().toISOString() + const line = data !== undefined + ? `[${now}] [ideOpen] ${message} ${JSON.stringify(data, null, 2)}` + : `[${now}] [ideOpen] ${message}` + console.log(line) + try { + fs.appendFileSync(getLogFile(), line + '\n') + } catch (e) { + console.warn('[ideOpen] 写入日志文件失败:', e) + } +} + +// ─── sql.js 原生加载 ── + +const WASM_DIR = path.join(__dirname, 'node_modules', 'sql.js', 'dist') + +let _SQL = null +async function getSQL() { + if (!_SQL) { + debugLog('初始化 SQL.js...') + const sqlJsCode = fs.readFileSync(path.join(WASM_DIR, 'sql-wasm.js'), 'utf-8') + const initSqlJs = new Function( + 'require', 'module', 'exports', + sqlJsCode + '\nreturn module.exports;' + )(require, { exports: {} }, {}).default || require + const wasmBinary = fs.readFileSync(path.join(WASM_DIR, 'sql-wasm.wasm')) + _SQL = await initSqlJs({ wasmBinary }) + debugLog('SQL.js 初始化完成') + } + return _SQL +} + +// ─── glob 路径解析(JetBrains 版本目录含通配符) ── + +function resolveGlobPath(pattern) { + if (!pattern.includes('*')) return pattern + + const starIdx = pattern.indexOf('*') + const before = pattern.substring(0, starIdx) + const after = pattern.substring(starIdx + 1) + const dir = path.dirname(before) + const prefix = path.basename(before) + + if (!fs.existsSync(dir)) return pattern + + try { + const entries = fs.readdirSync(dir).sort().reverse() + for (const entry of entries) { + if (entry.startsWith(prefix)) { + const resolved = path.join(dir, entry) + after + if (fs.existsSync(resolved)) return resolved + } + } + } catch {} + + return pattern +} + +// ─── 类型 ── + +// IDEItem: { code, name, command, dbPath, shell } +// ProjectItem: { name, path, uri, type, label } + +// ─── SQLite 读取 ── + +const RECENT_KEYS = [ + 'history.recentlyOpenedPathsList', + 'history.openedPathsList', + 'openedPathsList' +] + +async function readProjectsFromSQLite(dbPath) { + const SQL = await getSQL() + const resolvedPath = resolveGlobPath(dbPath) + const candidates = [resolvedPath] + // VSCode 共享存储 fallback + const home = process.env.HOME || process.env.USERPROFILE + if (home && resolvedPath.includes(path.join('Code', 'User', 'globalStorage'))) { + const shared = path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb') + if (fs.existsSync(shared) && !candidates.includes(shared)) candidates.push(shared) + } + + for (const filePath of candidates) { + if (!fs.existsSync(filePath)) { + debugLog(`数据库不存在: ${filePath}`) + continue + } + debugLog(`读取数据库: ${filePath}`) + const buffer = fs.readFileSync(filePath) + const db = new SQL.Database(buffer) + try { + for (const key of RECENT_KEYS) { + const results = db.exec(`SELECT value FROM ItemTable WHERE key = '${key}'`) + if (results.length > 0 && results[0].values.length > 0) { + const value = results[0].values[0][0] + const data = JSON.parse(value) + const entries = data.entries || [] + debugLog(`命中 key=${key}, entries=${entries.length}`) + if (entries.length > 0) return parseEntries(entries) + } + } + } finally { + db.close() + } + } + debugLog('未找到项目记录') + return [] +} + +// ─── JSON 读取 ── + +async function readProjectsFromJSON(jsonPath) { + debugLog(`读取 JSON: ${jsonPath}`) + const content = fs.readFileSync(jsonPath, 'utf-8') + const data = JSON.parse(content) + for (const key of RECENT_KEYS) { + const entries = data?.[key]?.entries || [] + if (entries.length > 0) { + debugLog(`JSON 命中 key=${key}, entries=${entries.length}`) + return parseEntries(entries) + } + } + debugLog('JSON 未找到项目记录') + return [] +} + +// ─── XML 读取(JetBrains) ── + +function readProjectsFromXML(xmlPath) { + const resolvedPath = resolveGlobPath(xmlPath) + if (!fs.existsSync(resolvedPath)) return [] + const content = fs.readFileSync(resolvedPath, 'utf-8') + + const pathRe = //g + const paths = [] + let m + while ((m = pathRe.exec(content)) !== null) { + const v = m[1] + if (v.startsWith('/') || v.match(/^[A-Z]:\\/)) paths.push(v) + } + + const nameByPath = {} + const entryRe = /]*>[\s\S]*?[\s\S]*?<\/entry>/g + let n + while ((n = entryRe.exec(content)) !== null) { + nameByPath[n[1]] = n[2] + } + + return paths.map(p => ({ + name: nameByPath[p] || path.basename(p), + path: p, + uri: p, + type: 'folder', + label: nameByPath[p] || '' + })) +} + +// ─── 统一解析 ── + +function parseEntries(entries) { + debugLog(`parseEntries 输入: ${entries.length} 条`) + const result = entries + .map(e => { + if (e == null) return null + if (typeof e === 'string') { + if (!e) return null + const isRemote = /^[a-z]+-remote:\/\//.test(e) + const isWorkspace = e.endsWith('.code-workspace') + const localPath = isRemote ? '' : uriToPath(e) + return { + name: path.basename(safeDecodeURIComponent(e).replace(/^file:\/\//, '').replace(/^[a-z]+-remote:\/\//, '')) || '未命名', + path: localPath, + uri: e, + type: isRemote ? 'remote' : isWorkspace ? 'workspace' : 'folder', + label: '' + } + } + const uri = e.folderUri || e.fileUri || e.workspace?.configPath || '' + if (!uri) return null + const decoded = safeDecodeURIComponent(uri) + const name = path.basename( + decoded.replace(/^file:\/\//, '').replace(/^[a-z]+-remote:\/\//, '') + ) + const isRemote = /^[a-z]+-remote:\/\//.test(uri) + const isWorkspace = uri.endsWith('.code-workspace') + const isFile = !!e.fileUri && !e.folderUri + const localPath = isRemote ? '' : uriToPath(uri) + return { + name: e.label || name || '未命名', + path: localPath, + uri, + type: isRemote ? 'remote' : isWorkspace ? 'workspace' : isFile ? 'file' : 'folder', + label: e.label || '' + } + }) + .filter(Boolean) + debugLog(`parseEntries 输出: ${result.length} 条`) + return result +} + +function safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str) + } catch { + return str + } +} + +function uriToPath(uri) { + try { + let p + if (uri.startsWith('file://')) { + p = fileURLToPath(uri) + } else { + const url = new URL(uri) + p = decodeURIComponent(url.pathname) + } + // 清理路径末尾的反斜杠(防止 Windows 风格路径残留 \) + return p.replace(/\\+$/, '') + } catch { + return uri + } +} + +// ─── 主入口 ── + +async function readProjects(filePath) { + debugLog(`readProjects 入口: ${filePath}`) + const resolvedPath = resolveGlobPath(filePath) + if (!fs.existsSync(resolvedPath)) { + debugLog(`文件不存在: ${resolvedPath}`) + throw new Error(`文件不存在: ${resolvedPath}`) + } + const ext = path.extname(resolvedPath).toLowerCase() + debugLog(`文件扩展名: ${ext}`) + if (ext === '.vscdb' || ext === '.db') { + const items = await readProjectsFromSQLite(resolvedPath) + debugLog(`SQLite 解析结果: ${items.length} 个项目`) + return items + } + if (ext === '.json') { + const items = await readProjectsFromJSON(resolvedPath) + debugLog(`JSON 解析结果: ${items.length} 个项目`) + return items + } + if (ext === '.xml') { + const items = readProjectsFromXML(resolvedPath) + debugLog(`XML 解析结果: ${items.length} 个项目`) + return items + } + try { + const items = await readProjectsFromSQLite(resolvedPath) + debugLog(`自动探测 SQLite 结果: ${items.length} 个项目`) + return items + } catch { + const items = await readProjectsFromJSON(resolvedPath) + debugLog(`自动探测 JSON 结果: ${items.length} 个项目`) + return items + } +} + +// ─── 删除项目记录 ── + +async function deleteProject(dbPath, uri) { + const resolvedPath = resolveGlobPath(dbPath) + const candidates = [resolvedPath] + const home = process.env.HOME || process.env.USERPROFILE + if (home && resolvedPath.includes(path.join('Code', 'User', 'globalStorage'))) { + const shared = path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb') + if (fs.existsSync(shared) && !candidates.includes(shared)) candidates.push(shared) + } + + for (const filePath of candidates) { + if (!fs.existsSync(filePath)) continue + const ext = path.extname(filePath).toLowerCase() + if (ext !== '.vscdb' && ext !== '.db') continue + + const SQL = await getSQL() + const buffer = fs.readFileSync(filePath) + const db = new SQL.Database(buffer) + + try { + for (const key of RECENT_KEYS) { + const results = db.exec(`SELECT value FROM ItemTable WHERE key = '${key}'`) + if (results.length === 0 || results[0].values.length === 0) continue + + const value = results[0].values[0][0] + const data = JSON.parse(value) + const entries = data.entries || [] + const before = entries.length + data.entries = entries.filter(e => { + if (typeof e === 'string') return e !== uri + const ep = e.folderUri || e.fileUri || e.workspace?.configPath + return ep !== uri + }) + if (data.entries.length === before) continue + + const updated = JSON.stringify(data) + db.run(`UPDATE ItemTable SET value = ? WHERE key = '${key}'`, [updated]) + const out = db.export() + fs.writeFileSync(filePath, Buffer.from(out)) + return + } + } finally { + db.close() + } + } + throw new Error('未找到匹配的记录或数据库不支持删除') +} + +// ─── IDE 配置管理 ── + +const STORAGE_KEY = 'ide-ides' +const LEGACY_STORAGE_KEY = 'vsc-ides' + +function getIDEs() { + try { + const data = window.ztools.dbStorage.getItem(STORAGE_KEY) + if (Array.isArray(data) && data.length > 0) { + debugLog(`getIDEs 从新 key 读取: ${data.length} 个`) + return data + } + const legacy = window.ztools.dbStorage.getItem(LEGACY_STORAGE_KEY) + if (Array.isArray(legacy) && legacy.length > 0) { + debugLog(`getIDEs 从旧 key 迁移: ${legacy.length} 个`) + window.ztools.dbStorage.setItem(STORAGE_KEY, legacy) + return legacy + } + debugLog('getIDEs 未找到配置') + return [] + } catch (e) { + debugLog(`getIDEs 异常: ${e}`) + return [] + } +} + +function saveIDEs(ides) { + debugLog(`saveIDEs: ${ides.length} 个`) + window.ztools.dbStorage.setItem(STORAGE_KEY, ides) +} + +// ─── 打开项目 ── + +const defaultShell = process.platform === 'darwin' ? 'zsh -l -i -c' : process.platform === 'linux' ? 'bash -l -i -c' : '' + +function openProject(command, uri, shell) { + const effectiveShell = shell || defaultShell + const isRemote = /^[a-z]+-remote:\/\//.test(uri) + const localPath = isRemote ? '' : uriToPath(uri) + + const run = (cmd, timeout = 10000) => new Promise((resolve, reject) => { + const fullCmd = effectiveShell ? `${effectiveShell} '${cmd}'` : cmd + debugLog(`执行: ${fullCmd}`) + exec(fullCmd, { env: process.env, windowsHide: true, timeout }, (err) => { + if (err) { + debugLog(`命令失败: ${err.message}`) + reject(err) + } else resolve() + }) + }) + + return new Promise((resolve, reject) => { + if (localPath) { + run(`${command} "${localPath}"`) + .then(() => resolve()) + .catch(err => reject(new Error(`启动失败: ${err.message}`))) + return + } + const isWorkspace = uri.endsWith('.code-workspace') + const flag = isWorkspace ? '--file-uri' : '--folder-uri' + run(`${command} ${flag} "${uri}"`) + .then(() => resolve()) + .catch(err => reject(new Error(`启动失败: ${err.message}`))) + }) +} + +// ─── 预设 ── + +const homeDir = () => process.env.HOME || process.env.USERPROFILE || '' +const jetBrainsDir = () => { + const home = homeDir() + if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'JetBrains') + if (process.platform === 'win32') return path.join(process.env.APPDATA || home, 'JetBrains') + return path.join(home, '.config', 'JetBrains') +} + +function getPresets() { + const appData = window.ztools.getPath('appData') + const jbDir = jetBrainsDir() + const home = homeDir() + return { + vscode: { + name: 'VSCode', + command: 'code', + dbPaths: [ + path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb'), + path.join(appData, 'Code', 'User', 'globalStorage', 'state.vscdb'), + path.join(appData, 'Code', 'storage.json') + ] + }, + cursor: { + name: 'Cursor', + command: 'cursor', + dbPaths: [ + path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb'), + path.join(appData, 'Cursor', 'storage.json') + ] + }, + vscodium: { + name: 'VSCodium', + command: 'codium', + dbPaths: [ + path.join(appData, 'VSCodium', 'User', 'globalStorage', 'state.vscdb'), + path.join(appData, 'VSCodium', 'storage.json') + ] + }, + idea: { + name: 'IntelliJ IDEA', + command: 'idea', + dbPaths: [path.join(jbDir, 'IntelliJIdea*', 'options', 'recentProjects.xml')] + }, + pycharm: { + name: 'PyCharm', + command: 'pycharm', + dbPaths: [path.join(jbDir, 'PyCharm*', 'options', 'recentProjects.xml')] + }, + webstorm: { + name: 'WebStorm', + command: 'webstorm', + dbPaths: [path.join(jbDir, 'WebStorm*', 'options', 'recentProjects.xml')] + }, + goland: { + name: 'GoLand', + command: 'goland', + dbPaths: [path.join(jbDir, 'GoLand*', 'options', 'recentProjects.xml')] + }, + qoder: { + name: 'Qoder', + command: 'qoder', + dbPaths: [ + path.join(appData, 'Qoder', 'User', 'globalStorage', 'state.vscdb'), + path.join(appData, 'Qoder', 'storage.json'), + path.join(appData, 'Qoder', 'User', 'globalStorage', 'storage.json') + ] + } + } +} + +function getQuickFillPresets() { + const home = process.env.HOME || process.env.USERPROFILE || '' + const appData = window.ztools.getPath('appData') + const jbDir = jetBrainsDir() + const vscDbPath = home + ? path.join(home, '.vscode-shared', 'sharedStorage', 'state.vscdb') + : path.join(appData, 'Code', 'User', 'globalStorage', 'state.vscdb') + return [ + { code: 'vsc', name: 'VS Code', command: 'code', dbPath: vscDbPath, shell: defaultShell }, + { code: 'cursor', name: 'Cursor', command: 'cursor', dbPath: path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb'), shell: defaultShell }, + { code: 'codium', name: 'VSCodium', command: 'codium', dbPath: path.join(appData, 'VSCodium', 'User', 'globalStorage', 'state.vscdb'), shell: defaultShell }, + { code: 'idea', name: 'IntelliJ IDEA', command: 'idea', dbPath: path.join(jbDir, 'IntelliJIdea*', 'options', 'recentProjects.xml'), shell: defaultShell }, + { code: 'pycharm', name: 'PyCharm', command: 'pycharm', dbPath: path.join(jbDir, 'PyCharm*', 'options', 'recentProjects.xml'), shell: defaultShell }, + { code: 'webstorm', name: 'WebStorm', command: 'webstorm', dbPath: path.join(jbDir, 'WebStorm*', 'options', 'recentProjects.xml'), shell: defaultShell }, + { code: 'goland', name: 'GoLand', command: 'goland', dbPath: path.join(jbDir, 'GoLand*', 'options', 'recentProjects.xml'), shell: defaultShell }, + { code: 'qoder', name: 'Qoder', command: 'qoder', dbPath: path.join(appData, 'Qoder', 'User', 'globalStorage', 'state.vscdb'), shell: defaultShell } + ] +} + +function detectPresetPaths() { + const presets = getPresets() + const result = {} + for (const [key, preset] of Object.entries(presets)) { + debugLog(`detectPresetPaths ${key}: 尝试 ${preset.dbPaths.length} 个路径`) + for (const p of preset.dbPaths) { + const resolved = resolveGlobPath(p) + const exists = fs.existsSync(resolved) + debugLog(` ${exists ? '✓' : '✗'} ${resolved}`) + if (exists) { + result[key] = resolved + break + } + } + } + debugLog('detectPresetPaths 结果:', result) + return result +} + +// ─── 动态 Feature 注册 ── + +const REG_CODES_KEY = 'ide-registered-codes' + +function registerFeatures() { + const ides = getIDEs() + const currentCodes = ides.filter(i => i.code).map(i => i.code) + debugLog(`registerFeatures: current=${currentCodes.join(',') || '(none)'}`) + const prevCodes = window.ztools.dbStorage.getItem(REG_CODES_KEY) || [] + + for (const ide of ides) { + if (!ide.code) continue + try { + window.ztools.setFeature({ + code: ide.code, + explain: `打开 ${ide.name || ide.code} 最近项目`, + cmds: [ide.code], + icon: 'logo.png' + }) + debugLog(`✅ 注册 feature: ${ide.code}`) + } catch (e) { + debugLog(`❌ 注册 ${ide.code} 失败: ${e}`) + } + } + + for (const oldCode of prevCodes) { + if (!currentCodes.includes(oldCode)) { + try { + window.ztools.removeFeature(oldCode) + debugLog(`🗑 删除旧 feature: ${oldCode}`) + } catch (e) { + debugLog(`❌ 删除 feature ${oldCode} 失败: ${e}`) + } + } + } + + window.ztools.dbStorage.setItem(REG_CODES_KEY, currentCodes) +} + +// ─── 启动时注册所有已保存 IDE 的动态指令 ── + +registerFeatures() + +// ─── 暴露给渲染进程 ── + +window.services = { + readProjects, + openProject, + deleteProject, + getIDEs, + saveIDEs, + registerFeatures, + getPresets, + getQuickFillPresets, + detectPresetPaths, + getAppDataPath: () => window.ztools.getPath('appData'), + getDefaultShell: () => defaultShell, + debugLog, + getLogFile +} diff --git a/plugins/ide-open/public/preload/sql-wasm.wasm b/plugins/ide-open/public/preload/sql-wasm.wasm new file mode 100755 index 00000000..b32b6647 Binary files /dev/null and b/plugins/ide-open/public/preload/sql-wasm.wasm differ diff --git a/plugins/ide-open/scripts/gen-changelog.mjs b/plugins/ide-open/scripts/gen-changelog.mjs new file mode 100644 index 00000000..5609ccd5 --- /dev/null +++ b/plugins/ide-open/scripts/gen-changelog.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +/** + * 从 ztools-last-publish tag 之后的 commit 自动生成 CHANGELOG.md 条目 + * 约定 commit 格式: type(scope): message + * feat: → Added + * fix: → Fixed + * refactor: → Changed + * chore: → (跳过,不记入) + * docs: → (跳过) + */ +import { execSync } from 'node:child_process' +import { readFileSync, writeFileSync, existsSync } from 'node:fs' + +const TAG = 'ztools-last-publish' +const VERSION = JSON.parse(readFileSync('package.json', 'utf-8')).version + +// 获取上次发布后的 commit messages +function getCommits() { + let range + try { + // 检查 tag 是否存在 + execSync(`git rev-parse --verify refs/tags/${TAG}`, { stdio: 'pipe' }) + range = `${TAG}..HEAD` + } catch { + // tag 不存在,取所有 commit + range = 'HEAD' + } + + const log = execSync(`git log ${range} --pretty=format:"%s" --no-merges`, { encoding: 'utf-8' }) + return log.trim().split('\n').filter(Boolean) +} + +const TYPE_MAP = { + feat: 'Added', + fix: 'Fixed', + refactor: 'Changed', + perf: 'Changed', +} + +const groups = { Added: [], Fixed: [], Changed: [] } + +for (const line of getCommits()) { + const m = line.match(/^(\w+)(?:\(([^)]+)\))?:\s*(.+)$/) + if (!m) continue + const [, type, scope, msg] = m + const section = TYPE_MAP[type] + if (!section) continue // chore, docs, ci 等跳过 + const prefix = scope ? `**${scope}** ` : '' + groups[section].push(`- ${prefix}${msg}`) +} + +// 检查是否已有该版本条目 +const existing = existsSync('CHANGELOG.md') ? readFileSync('CHANGELOG.md', 'utf-8') : '' +if (existing.includes(`## ${VERSION}`)) { + console.log(`CHANGELOG.md 中已存在 v${VERSION} 条目,跳过生成`) + process.exit(0) +} + +// 生成新条目 +const sections = Object.entries(groups) + .filter(([, items]) => items.length > 0) + .map(([title, items]) => `### ${title}\n${items.join('\n')}`) + .join('\n\n') + +if (!sections) { + console.log('没有可记录的变更(feat/fix/refactor)') + process.exit(0) +} + +const entry = `## ${VERSION}\n\n${sections}\n` +const output = existing + ? existing.replace(/^# Changelog\n/, `# Changelog\n\n${entry}`) + : `# Changelog\n\n${entry}` + +writeFileSync('CHANGELOG.md', output) +console.log(`✓ 已生成 v${VERSION} 的 CHANGELOG 条目:`) +console.log(entry) diff --git a/plugins/ide-open/src/App.tsx b/plugins/ide-open/src/App.tsx new file mode 100644 index 00000000..54dd23d7 --- /dev/null +++ b/plugins/ide-open/src/App.tsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react' +import Settings from './Settings' +import ProjectList from './ProjectList' +import { getIDEs, type IDEItem } from './store' + +type View = + | { page: 'settings' } + | { page: 'projects'; ide: IDEItem } + +export default function App() { + const [view, setView] = useState({ page: 'settings' }) + + useEffect(() => { + window.ztools.onPluginEnter((action) => { + window.services?.debugLog('onPluginEnter', action.code) + if (action.code === 'ideopen') { setView({ page: 'settings' }); return } + const ides = getIDEs() + window.services?.debugLog('当前 IDE 列表', ides.map(i => i.code)) + const ide = ides.find(i => i.code === action.code) + if (ide) { + window.services?.debugLog('匹配到 IDE', ide) + setView({ page: 'projects', ide }) + } else { + window.services?.debugLog('未找到匹配的 IDE', action.code) + } + }) + window.ztools.onPluginOut(() => { + setView({ page: 'settings' }) + }) + }, []) + + if (view.page === 'projects') return setView({ page: 'settings' })} /> + + return +} diff --git a/plugins/ide-open/src/ProjectList/index.css b/plugins/ide-open/src/ProjectList/index.css new file mode 100644 index 00000000..f587e48f --- /dev/null +++ b/plugins/ide-open/src/ProjectList/index.css @@ -0,0 +1,87 @@ +.project-list { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +.pl-ide-name { font-weight: 600; font-size: 14px; } +.pl-count { color: var(--fg2); font-size: 12px; } + +.btn-edit-top { + margin-left: auto; + color: var(--blue); + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid var(--border); +} +.btn-edit-top:hover { border-color: var(--blue); background: rgba(22,119,255,.04); } + +.pl-loading, .pl-empty { + text-align: center; + padding: 32px; + color: var(--fg2); + font-size: 12px; +} + +.pl-items { + flex: 1; + overflow-y: auto; + padding: 0 12px 12px; +} + +.pl-item { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px; + border-radius: 4px; + cursor: pointer; + margin-bottom: 2px; + transition: background .15s; +} + +.pl-item:hover { background: var(--bg2); } +.pl-item-selected { background: rgba(22,119,255,.08) !important; } + +.pl-item-icon { font-size: 16px; flex-shrink: 0; width: 20px; text-align: center; color: var(--blue); } + +.pl-item-info { flex: 1; min-width: 0; } +.pl-item-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pl-item-path { font-size: 11px; color: var(--fg2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px; } + +.pl-item-del { + font-size: 12px; + color: var(--red); + opacity: 0; + padding: 0 4px; + border-radius: 3px; + transition: opacity .15s; +} +.pl-item:hover .pl-item-del, +.pl-item-selected .pl-item-del { opacity: .5; } +.pl-item .pl-item-del:hover { opacity: 1; background: rgba(255,77,79,.08); } + +.pl-item-badge { + font-size: 10px; + padding: 0 5px; + line-height: 18px; + border: 1px solid rgba(22,119,255,.25); + color: var(--blue); + border-radius: 3px; + flex-shrink: 0; +} + +.project-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + gap: 8px; + color: var(--red); + font-size: 12px; + padding: 20px; + text-align: center; +} diff --git a/plugins/ide-open/src/ProjectList/index.tsx b/plugins/ide-open/src/ProjectList/index.tsx new file mode 100644 index 00000000..e6cd4040 --- /dev/null +++ b/plugins/ide-open/src/ProjectList/index.tsx @@ -0,0 +1,175 @@ +import { useEffect, useState, useRef } from 'react' +import { readProjects, openProject, deleteProject, type IDEItem, type ProjectItem } from '../store' +import './index.css' + +function IconFolder() { + return ( + + + + + ) +} + +function IconFile() { + return ( + + + + + ) +} + +function IconRemote() { + return ( + + + + + + ) +} + +export default function ProjectList({ ide, onBack, onEdit }: { ide: IDEItem; onBack?: () => void; onEdit?: () => void }) { + const [projects, setProjects] = useState([]) + const [filtered, setFiltered] = useState([]) + const [search, setSearch] = useState('') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [selected, setSelected] = useState(-1) + const listRef = useRef(null) + + // refs for document-level keydown (避免频繁重新注册) + const filteredRef = useRef(filtered) + const selectedRef = useRef(selected) + filteredRef.current = filtered + selectedRef.current = selected + + const load = () => { + window.services?.debugLog('ProjectList load', { code: ide.code, command: ide.command, dbPath: ide.dbPath, shell: ide.shell }) + if (!ide.command || !ide.dbPath) { + setError(`请先在设置页配置「${ide.name}」的数据文件路径`) + setLoading(false) + return + } + setLoading(true) + setError('') + readProjects(ide.dbPath) + .then(items => { + window.services?.debugLog(`ProjectList 加载完成: ${items.length} 个项目`) + setProjects(items); setFiltered(items) + }) + .catch((err: Error) => { + window.services?.debugLog('ProjectList 读取失败', err.message) + setError(`读取失败: ${err.message}`) + }) + .finally(() => setLoading(false)) + } + + // 设置子输入框 + 加载数据 + useEffect(() => { + setSearch('') + window.ztools.setSubInput((input: { text: string }) => setSearch(input.text), '搜索项目,Enter 打开...', true) + window.ztools.setSubInputValue('') + window.ztools.setExpendHeight(500) + load() + return () => { window.ztools.removeSubInput() } + }, [ide.code]) + + // 过滤 + useEffect(() => { + const q = search.toLowerCase().trim() + if (!q) { setFiltered(projects); return } + const terms = q.split(/\s+/) + setFiltered(projects.filter(p => + terms.every(t => + p.name.toLowerCase().includes(t) || p.path.toLowerCase().includes(t) || p.label.toLowerCase().includes(t) + ) + )) + setSelected(-1) + }, [search, projects]) + + // 滚动到选中项 + useEffect(() => { + if (selected < 0 || !listRef.current) return + const el = listRef.current.children[selected] as HTMLElement + if (el) el.scrollIntoView({ block: 'nearest' }) + }, [selected]) + + const handleOpen = async (project: ProjectItem) => { + window.services?.debugLog('handleOpen', { command: ide.command, uri: project.uri, shell: ide.shell }) + try { + await openProject(ide.command, project.uri, ide.shell) + window.ztools.hideMainWindow() + window.ztools.outPlugin() + } catch (err: any) { + window.services?.debugLog('handleOpen 失败', err.message) + alert(`打开失败: ${err.message}`) + } + } + + const handleDelete = async (project: ProjectItem) => { + if (!confirm(`确定从历史记录中删除「${project.name}」?`)) return + try { + await deleteProject(ide.dbPath, project.uri) + load() + } catch (err: any) { + alert(`删除失败: ${err.message}`) + } + } + + // 键盘导航(document 级,因为焦点在 ZTools 主搜索框上) + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + const f = filteredRef.current + const s = selectedRef.current + if (e.key === 'ArrowDown') { + e.preventDefault(); setSelected(i => i >= f.length - 1 ? 0 : i + 1) + } else if (e.key === 'ArrowUp') { + e.preventDefault(); setSelected(i => i <= 0 ? f.length - 1 : i - 1) + } else if (e.key === 'Enter' && s >= 0) { + e.preventDefault(); handleOpen(f[s]) + } + } + document.addEventListener('keydown', onKeyDown) + return () => document.removeEventListener('keydown', onKeyDown) + }, []) + + return ( +
+
+ {onBack && } + {ide.name} + {filtered.length} 个项目 + {onEdit && } +
+ + {loading &&
加载中...
} + {error && !loading &&

{error}

} + {!loading && !error && filtered.length === 0 && ( +
{search ? '没有匹配的项目' : '暂无最近项目'}
+ )} + +
+ {filtered.map((p, i) => ( +
handleOpen(p)} + onContextMenu={e => { e.preventDefault(); handleDelete(p) }} + onMouseEnter={() => setSelected(i)}> +
+ {p.type === 'remote' ? : p.type === 'workspace' || p.type === 'file' ? : } +
+
+
{p.name}
+
{p.path || p.uri}
+
+ + {p.type === 'remote' && 远程} +
+ ))} +
+
+ ) +} diff --git a/plugins/ide-open/src/Settings/index.css b/plugins/ide-open/src/Settings/index.css new file mode 100644 index 00000000..54b1d8a8 --- /dev/null +++ b/plugins/ide-open/src/Settings/index.css @@ -0,0 +1,121 @@ +.settings { padding: 12px; } + +.settings h2 { + margin: 0 0 2px; + font-size: 14px; + font-weight: 600; +} + +.settings .subtitle { + color: var(--fg2); + font-weight: 400; + font-size: 12px; +} + +.empty-hint { color: var(--fg2); text-align: center; padding: 32px 0; } + +.btn-toggle { + width: 100%; + padding: 4px; + text-align: center; + border: 1px dashed var(--border); + border-radius: 4px; + color: var(--fg2); + font-size: 12px; + margin-bottom: 8px; +} +.btn-toggle:hover { border-color: var(--blue); color: var(--blue); } + +.add-form { + padding: 10px; + border: 1px solid var(--border); + border-radius: 4px; + margin-bottom: 8px; + background: var(--bg2); +} + +.add-form label { + display: block; + margin-bottom: 6px; + font-size: 12px; +} +.add-form label span { color: var(--fg2); margin-bottom: 2px; display: block; } +.add-form input { display: block; width: 100%; } + +.quick-fill { + display: flex; + gap: 4px; + flex-wrap: wrap; + margin-bottom: 8px; +} +.qf-label { color: var(--fg2); line-height: 22px; font-size: 12px; } + +.btn-qf { + padding: 1px 8px; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--fg2); + font-size: 12px; + line-height: 20px; +} +.btn-qf:hover { border-color: var(--blue); color: var(--blue); } + +.btn-submit { + width: 100%; + padding: 4px; + margin-top: 4px; + background: var(--blue); + color: #fff; + border-radius: 4px; + font-size: 12px; + line-height: 22px; +} +.btn-submit:hover { opacity: .85; } + +.ide-table { width: 100%; border-collapse: collapse; font-size: 12px; } + +.ide-table th { + text-align: left; + padding: 5px 8px; + font-weight: 500; + color: var(--fg2); + border-bottom: 1px solid var(--border); + font-size: 11px; +} + +.ide-table td { + padding: 4px 8px; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +.ide-table td code { font-weight: 600; font-family: monospace; font-size: 12px; color: var(--blue); } + +.ide-table td input { + width: 100%; + padding: 2px 6px; + border: 1px solid transparent; + background: transparent; + border-radius: 2px; +} +.ide-table td input:focus { border-color: var(--blue); background: var(--bg); } + +.td-path { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.td-actions { white-space: nowrap; text-align: right; } + +.btn-edit { + color: var(--blue); + font-size: 12px; + padding: 1px 6px; + border-radius: 3px; +} +.btn-edit:hover { background: rgba(22,119,255,.06); } + +.btn-remove { + color: var(--red); + font-size: 13px; + padding: 1px 4px; + border-radius: 3px; + opacity: .6; +} +.btn-remove:hover { opacity: 1; background: rgba(255,77,79,.06); } diff --git a/plugins/ide-open/src/Settings/index.tsx b/plugins/ide-open/src/Settings/index.tsx new file mode 100644 index 00000000..ee26d526 --- /dev/null +++ b/plugins/ide-open/src/Settings/index.tsx @@ -0,0 +1,141 @@ +import { useEffect, useState } from 'react' +import { getIDEs, saveIDEs, getQuickFillPresets, getAppDataPath, getDefaultShell, type IDEItem } from '../store' +import './index.css' + +const emptyForm = (): IDEItem => ({ code: '', name: '', command: '', dbPath: '', shell: '' }) + +export default function Settings({ onBack, editIDE }: { onBack?: () => void; editIDE?: IDEItem }) { + const [ides, setIDEs] = useState([]) + const [showForm, setShowForm] = useState(false) + const [form, setForm] = useState(emptyForm()) + const [editingIdx, setEditingIdx] = useState(-1) + const defaultShell = getDefaultShell() + const appData = getAppDataPath() + + useEffect(() => { + setIDEs(getIDEs()) + }, []) + + useEffect(() => { + if (!editIDE) return + const idx = ides.findIndex(i => i.code === editIDE.code) + if (idx >= 0) { + setForm({ ...editIDE }) + setEditingIdx(idx) + setShowForm(true) + } + }, [editIDE]) + + const save = () => { + if (!form.code.trim()) { alert('请输入别名'); return } + if (!form.command.trim()) { alert('请输入启动命令'); return } + if (!form.dbPath.trim()) { alert('请输入历史文件路径'); return } + const entry: IDEItem = { code: form.code.trim(), name: form.name.trim() || form.code.trim(), command: form.command.trim(), dbPath: form.dbPath.trim(), shell: form.shell.trim() } + let list + if (editingIdx >= 0) { + list = ides.map((item, i) => i === editingIdx ? entry : item) + } else { + list = [...ides, entry] + } + setIDEs(list) + saveIDEs(list) + setForm(emptyForm()) + setEditingIdx(-1) + setShowForm(false) + } + + const del = (idx: number) => { + const list = ides.filter((_, i) => i !== idx) + setIDEs(list) + saveIDEs(list) + } + + const startEdit = (idx: number) => { + const ide = ides[idx] + setForm({ code: ide.code, name: ide.name, command: ide.command, dbPath: ide.dbPath, shell: ide.shell || defaultShell }) + setEditingIdx(idx) + setShowForm(true) + } + + const startAdd = () => { + setForm(emptyForm()) + setEditingIdx(-1) + setShowForm(true) + } + + const fill = (preset: any) => { + setForm({ code: preset.code, name: preset.name, command: preset.command, dbPath: preset.dbPath, shell: preset.shell || '' }) + } + + const isEditing = editingIdx >= 0 + const defaults = getQuickFillPresets() + + return ( +
+
+ {onBack && } +

ideOpen 配置 IDE

+
+ + + + {showForm && ( +
+ {!isEditing && ( +
+ 快速填入: + {defaults.map(p => ( + + ))} +
+ )} + + + + + + +
+ )} + + {ides.length === 0 && ( +

还没有配置,点「+ 新增配置」添加

+ )} + + {ides.length > 0 && ( + + + + + + + + + + + + + {ides.map((ide, idx) => ( + + + + + + + + + ))} + +
别名名称启动命令shell历史文件路径
{ide.code}{ide.name}{ide.command}{ide.shell || '-'}{ide.dbPath} + + +
+ )} +
+ ) +} diff --git a/plugins/ide-open/src/env.d.ts b/plugins/ide-open/src/env.d.ts new file mode 100644 index 00000000..72914eee --- /dev/null +++ b/plugins/ide-open/src/env.d.ts @@ -0,0 +1,26 @@ +/// +/// + +interface Services { + readProjects: (dbPath: string) => Promise + openProject: (command: string, uri: string, shell?: string) => Promise + deleteProject: (dbPath: string, uri: string) => Promise + getIDEs: () => any[] + saveIDEs: (ides: any[]) => void + registerFeatures: () => void + getPresets: () => Record + getQuickFillPresets: () => any[] + detectPresetPaths: () => Record + getAppDataPath: () => string + getDefaultShell: () => string + debugLog: (message: string, data?: any) => void + getLogFile: () => string +} + +declare global { + interface Window { + services: Services + } +} + +export {} diff --git a/plugins/ide-open/src/main.css b/plugins/ide-open/src/main.css new file mode 100644 index 00000000..bd6d869d --- /dev/null +++ b/plugins/ide-open/src/main.css @@ -0,0 +1,171 @@ +:root { + --blue: #1677ff; + --red: #ff4d4f; + --fg: #333; + --fg2: #999; + --bg: #fff; + --bg2: #fafafa; + --border: #e8e8e8; + --shadow: rgba(0,0,0,.06); +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--fg); + font: 13px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +::-webkit-scrollbar { width: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #d9d9d9; border-radius: 4px; } + +input { + font: inherit; + color: var(--fg); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 3px 7px; + outline: none; + transition: border .2s; +} +input:focus { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(22,119,255,.1); } +input::placeholder { color: #bbb; } + +button { + font: inherit; + cursor: pointer; + border: none; + background: none; + color: inherit; +} +button:active { opacity: .7; } + +/* ── 首页 ── */ + +.app-home { padding: 12px; } + +.app-home h2 { + margin: 0 0 8px; + font-size: 14px; + font-weight: 600; +} + +.app-home .subtitle { + color: var(--fg2); + font-weight: 400; + font-size: 12px; +} + +.app-home .empty-hint { + color: var(--fg2); + text-align: center; + padding: 32px 0; +} + +.ide-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 4px; + margin-bottom: 6px; + transition: border-color .2s; +} +.ide-card:hover { border-color: var(--blue); } + +.ide-card-body { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + flex: 1; +} + +.ide-card-body:hover .ide-card-name { color: var(--blue); } + +.ide-card-code { + font-weight: 600; + font-family: monospace; + font-size: 12px; + color: var(--blue); + background: rgba(22,119,255,.06); + padding: 0 6px; + border-radius: 3px; + line-height: 20px; +} + +.ide-card-name { font-size: 13px; transition: color .2s; } + +.ide-card-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.ide-card-btn { + font-size: 11px; + color: var(--fg2); + padding: 1px 6px; + border-radius: 3px; +} +.ide-card-btn:hover { background: rgba(22,119,255,.06); color: var(--blue); } + +.ide-card-arrow { + color: #ccc; + font-size: 14px; + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; +} +.ide-card-arrow:hover { background: var(--bg2); color: var(--blue); } + +.btn-add-ide { + display: block; + width: 100%; + padding: 4px; + text-align: center; + border: 1px dashed var(--border); + border-radius: 4px; + color: var(--fg2); + font-size: 12px; + margin-top: 4px; +} +.btn-add-ide:hover { border-color: var(--blue); color: var(--blue); } + +/* ── 顶部栏 ── */ + +.top-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px 4px; + margin-bottom: 4px; +} + +.btn-back { + color: var(--blue); + font-size: 12px; + padding: 2px 4px; +} +.btn-back:hover { opacity: .75; } + +@media (prefers-color-scheme: dark) { + :root { + --fg: #e0e0e0; + --fg2: #888; + --bg: #1a1a1a; + --bg2: #222; + --border: #333; + --shadow: transparent; + } + input { background: #222; border-color: #444; } + input:focus { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(22,119,255,.15); } + input::placeholder { color: #666; } + .ide-card-code { background: rgba(22,119,255,.12); } +} diff --git a/plugins/ide-open/src/main.tsx b/plugins/ide-open/src/main.tsx new file mode 100644 index 00000000..73fb2013 --- /dev/null +++ b/plugins/ide-open/src/main.tsx @@ -0,0 +1,6 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import './main.css' +import App from './App' + +ReactDOM.createRoot(document.getElementById('root')!).render() diff --git a/plugins/ide-open/src/store.ts b/plugins/ide-open/src/store.ts new file mode 100644 index 00000000..5d3f4796 --- /dev/null +++ b/plugins/ide-open/src/store.ts @@ -0,0 +1,74 @@ +export interface IDEItem { + code: string + name: string + command: string + dbPath: string + shell?: string +} + +export interface ProjectItem { + name: string + path: string + uri: string + type: 'folder' | 'file' | 'workspace' | 'remote' + label: string +} + +const STORAGE_KEY = 'ide-ides' + +export function getIDEs(): IDEItem[] { + try { + const data = window.ztools.dbStorage.getItem(STORAGE_KEY) + return Array.isArray(data) ? data : [] + } catch { return [] } +} + +export function saveIDEs(ides: IDEItem[]) { + window.ztools.dbStorage.setItem(STORAGE_KEY, ides) + window.services?.registerFeatures() +} + +export function addIDE(ide: IDEItem) { + const ides = getIDEs() + const idx = ides.findIndex(i => i.code === ide.code) + if (idx >= 0) ides[idx] = ide + else ides.push(ide) + saveIDEs(ides) +} + +export function removeIDE(code: string) { + saveIDEs(getIDEs().filter(i => i.code !== code)) +} + +export function getPresets() { + return window.services?.getPresets() || {} +} + +export function getQuickFillPresets() { + return window.services?.getQuickFillPresets() || [] +} + +export async function detectPresetPaths() { + return window.services?.detectPresetPaths() || {} +} + +export async function readProjects(dbPath: string): Promise { + if (!window.services) throw new Error('preload 未就绪') + return window.services.readProjects(dbPath) +} + +export async function openProject(command: string, uri: string, shell?: string) { + return window.services.openProject(command, uri, shell) +} + +export async function deleteProject(dbPath: string, uri: string) { + return window.services.deleteProject(dbPath, uri) +} + +export function getAppDataPath() { + return window.services?.getAppDataPath() || '' +} + +export function getDefaultShell() { + return window.services?.getDefaultShell() || '' +} diff --git a/plugins/ide-open/tsconfig.json b/plugins/ide-open/tsconfig.json new file mode 100644 index 00000000..d49c64de --- /dev/null +++ b/plugins/ide-open/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": false, + "noImplicitAny": false, + "types": ["@ztools-center/ztools-api-types"] + }, + "include": ["src"] +} diff --git a/plugins/ide-open/vite.config.js b/plugins/ide-open/vite.config.js new file mode 100644 index 00000000..2d511098 --- /dev/null +++ b/plugins/ide-open/vite.config.js @@ -0,0 +1,8 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + base: './' +})