主进程实现对 VSCode 和 JetBrains 项目的基本查找
This commit is contained in:
63
package-lock.json
generated
63
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"fast-xml-parser": "^5.5.9",
|
||||
"highlight.js": "^11.11.1",
|
||||
"load-json-file": "^7.0.1",
|
||||
"make-dir": "^5.1.0",
|
||||
@@ -5708,6 +5709,41 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.9",
|
||||
"resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz",
|
||||
"integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
@@ -7597,6 +7633,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
|
||||
"integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -8923,6 +8974,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"fast-xml-parser": "^5.5.9",
|
||||
"highlight.js": "^11.11.1",
|
||||
"load-json-file": "^7.0.1",
|
||||
"make-dir": "^5.1.0",
|
||||
|
||||
111
src/main/code-launchpad/ide-projects.ts
Normal file
111
src/main/code-launchpad/ide-projects.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import os from 'os'
|
||||
import type { IdeProjectsDto } from '@my-type/ide-projects'
|
||||
import { loadJsonFile } from 'load-json-file'
|
||||
import { XMLParser } from 'fast-xml-parser'
|
||||
import { VSCodeGlobalStorageJson } from '@my-type/vscode-globalstorage-json'
|
||||
import { JetBrainsIdeOptionsRecentProjects } from '@my-type/jetbrains-ide-options-recentProjects'
|
||||
import { JetBrainsIDEDisplayNameEnum, JetBrainsProductCode, toProductCode } from '@my-type/jetbrains-state-tools'
|
||||
|
||||
const xmlParser = new XMLParser({ ignoreAttributes: false })
|
||||
|
||||
// VSCode 用来保存打开过的工作区的文件路径
|
||||
const VSCODE_GLOBALSTORAGE_PATH = path.join(
|
||||
os.homedir(),
|
||||
'AppData/Roaming/Code/User/globalStorage/storage.json'
|
||||
)
|
||||
|
||||
// JetBrains IDEs 的默认数据保存目录
|
||||
// 每个 IDE 的每个版本都在该目录中拥有一个子目录
|
||||
const JETBRAINS_IDES_DATA_PATH = path.join(os.homedir(), 'AppData/Roaming/JetBrains')
|
||||
|
||||
/**
|
||||
* 从 VSCode 的 GlobalStorage 中读取所有打开过的工作区,整理后返回。
|
||||
*/
|
||||
export async function getVscodeProjects(): Promise<IdeProjectsDto> {
|
||||
const result: IdeProjectsDto = []
|
||||
const data: VSCodeGlobalStorageJson = await loadJsonFile(VSCODE_GLOBALSTORAGE_PATH)
|
||||
for (const workspace of Object.entries(data.profileAssociations.workspaces)) {
|
||||
// VSCode 存储的工作区的一个示例:
|
||||
// "vscode-remote://wsl%2Bubuntu-24.04/home/mango/pythonTest123": "__default__profile__"
|
||||
// 目前不清楚值的具体含义,但显然值对我们没有帮助。
|
||||
// 所以,将 path (项目路径)设置为键,然后取路径的最后一层目录为名称,构建数据并返回。
|
||||
|
||||
// JetBrains IDEs 的终端调用不支持 file:/// 命令,因此在此将协议名 file:/// 去除,方便用 JetBrains IDEs 打开它们。
|
||||
// 还有一种协议名是 vscode-remote:// 将会保留,以作为 VSCode Remote 的标识
|
||||
const path = decodeURIComponent(workspace[0]).replace('file:///', '')
|
||||
const name = <string>path.split('/').at(-1)
|
||||
result.push({ name, path, ide: ['VSC'] })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getJetBrainsProjects(): Promise<IdeProjectsDto> {
|
||||
const result: IdeProjectsDto = []
|
||||
// 意外的是,JetBrains Toolbox 并不会自己保存 JetBrains IDEs 打开过的项目的历史记录,哪怕是在 Toolbox 中打开的。
|
||||
// 据 AI 总结,工具箱的项目列表系读取已安装的所有 JetBrains IDE 的项目历史,并综合列出的。
|
||||
// 所以,我们也要这么做。
|
||||
const items = await fs.readdir(JETBRAINS_IDES_DATA_PATH, { withFileTypes: true })
|
||||
const subDirs: string[] = []
|
||||
// 只要目录,不要文件
|
||||
for (const item of items) {
|
||||
if (item.isDirectory()) {
|
||||
subDirs.push(item.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工具函数
|
||||
const _ = (subDir: string): JetBrainsProductCode | null => {
|
||||
// 获取枚举成员的变量名称
|
||||
for (const ide in Object.keys(JetBrainsIDEDisplayNameEnum)) {
|
||||
if (subDir.toLowerCase().includes(ide)) {
|
||||
// 查找与之对应的产品代码并返回
|
||||
// 由于已经做过判定,所以可得返回结果非空
|
||||
return <JetBrainsProductCode>toProductCode(ide)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
for (const subDir of subDirs) {
|
||||
// 如果目录不受支持,直接排除
|
||||
// 不受支持的表现之一就是目录名称中不包含工具箱支持的 IDE 的名称
|
||||
const ide = _(subDir)
|
||||
if (ide === null) continue
|
||||
// 从目录中尝试读取 options/recentProjects.xml
|
||||
const rpp = path.join(JETBRAINS_IDES_DATA_PATH, subDir, 'options/recentProjects.xml')
|
||||
try {
|
||||
// 从 xml 中解析数据
|
||||
const data: JetBrainsIdeOptionsRecentProjects = xmlParser.parse(
|
||||
await fs.readFile(rpp, 'utf-8')
|
||||
)
|
||||
for (const datum of data.application.component.option) {
|
||||
for (const entry of datum.map.entry) {
|
||||
const name = <string>entry.value.RecentProjectMetaInfo['@_frameTitle'].split(' – ').at(0)
|
||||
const path = entry['@_key'].replace('$USER_HOME$', os.homedir())
|
||||
// 认为包含此(安装/设置?)目录的是未保存的编辑,例如 light-edit 模式
|
||||
// 正常来说不会在这样的目录下保存项目的……吧?而且俺寻思 IDE 用这种变量的话也不像是人为刻意保存到此目录。
|
||||
if (path.includes('$APPLICATION_CONFIG_DIR$/')) continue
|
||||
result.push({
|
||||
name,
|
||||
path,
|
||||
ide: [ide]
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 结合 {@link getVscodeProjects} 和 {@link getJetBrainsProjects} 的返回结果,
|
||||
* 剔除重复项后,获取项目列表。
|
||||
*/
|
||||
export async function getProjects(): Promise<IdeProjectsDto> {
|
||||
const result: IdeProjectsDto = []
|
||||
return result
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from './code-launchpad/ide-versions-check'
|
||||
import { fanToolsIcon } from './resources'
|
||||
import path from 'path'
|
||||
import { getJetBrainsProjects, getVscodeProjects } from './code-launchpad/ide-projects'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
// @ts-ignore 保存引用,禁用报错
|
||||
@@ -92,7 +93,7 @@ function createTray(): Tray {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.electron')
|
||||
|
||||
@@ -128,7 +129,9 @@ app.whenReady().then(() => {
|
||||
ipcMain.handle('codeLaunchpad:checkIDEs', checkIDEs)
|
||||
ipcMain.handle('codeLaunchpad:getIDEsVersion', getIDEsVersion)
|
||||
ipcMain.handle('codeLaunchpad:checkIDEsVersion', checkIDEsVersion)
|
||||
ipcMain.handle('codeLaunchpad:getVSCodeProjects', getVscodeProjects)
|
||||
ipcMain.handle('codeLaunchpad:getJetBrainsProjects', getJetBrainsProjects)
|
||||
|
||||
checkIDEs()
|
||||
checkIDEsVersion()
|
||||
await checkIDEs()
|
||||
await checkIDEsVersion()
|
||||
})
|
||||
|
||||
11
src/my-type/ide-projects.d.ts
vendored
Normal file
11
src/my-type/ide-projects.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { JetBrainsProductCode } from '@my-type/jetbrains-state-tools'
|
||||
|
||||
type Ide = JetBrainsProductCode | 'VSC'
|
||||
|
||||
export interface IdeProjectDto {
|
||||
name: string
|
||||
path: string
|
||||
ide: Ide[]
|
||||
}
|
||||
|
||||
export type IdeProjectsDto = IdeProjectDto[]
|
||||
26
src/my-type/jetbrains-ide-options-recentProjects.d.ts
vendored
Normal file
26
src/my-type/jetbrains-ide-options-recentProjects.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
// 这些数据结构是由 XMLParser 解析成的 Javascript 对象,解析设置允许属性。
|
||||
// 不完整,因为我也看不懂很多东西是干嘛用的,写完整了也用不到,所以把用到的定义一下就酱了
|
||||
|
||||
export interface RecentProjectMetaInfo {
|
||||
option: []
|
||||
frame: object
|
||||
'@_frameTitle': string
|
||||
}
|
||||
|
||||
export interface JetBrainsIdeOptionsRecentProjects {
|
||||
application: {
|
||||
component: {
|
||||
option: [
|
||||
{
|
||||
map: {
|
||||
entry: {
|
||||
value: { RecentProjectMetaInfo: RecentProjectMetaInfo }
|
||||
'@_key': string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
]
|
||||
'@_name': 'RecentProjectsManager'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,3 +25,21 @@ export enum JetBrainsIDEDisplayNameEnum {
|
||||
phpstorm = 'PhpStorm',
|
||||
webstorm = 'WebStorm'
|
||||
}
|
||||
|
||||
export function toProductCode(anything: string): JetBrainsProductCode | null {
|
||||
if ([JetBrainsIDEDisplayNameEnum.pycharm, 'pycharm'].includes(anything)) return 'PY'
|
||||
if ([JetBrainsIDEDisplayNameEnum.idea, 'idea'].includes(anything)) return 'IU'
|
||||
if ([JetBrainsIDEDisplayNameEnum.clion, 'clion'].includes(anything)) return 'CL'
|
||||
if ([JetBrainsIDEDisplayNameEnum.phpstorm, 'phpstorm'].includes(anything)) return 'PS'
|
||||
if ([JetBrainsIDEDisplayNameEnum.webstorm, 'webstorm'].includes(anything)) return 'WS'
|
||||
return null
|
||||
}
|
||||
|
||||
export function toProductDisplayName(anything: string): string | null {
|
||||
if (['PY', 'pycharm'].includes(anything)) return JetBrainsIDEDisplayNameEnum.pycharm
|
||||
if (['IU', 'idea'].includes(anything)) return JetBrainsIDEDisplayNameEnum.idea
|
||||
if (['CL', 'clion'].includes(anything)) return JetBrainsIDEDisplayNameEnum.clion
|
||||
if (['PS', 'phpstorm'].includes(anything)) return JetBrainsIDEDisplayNameEnum.phpstorm
|
||||
if (['WS', 'webstorm'].includes(anything)) return JetBrainsIDEDisplayNameEnum.webstorm
|
||||
return null
|
||||
}
|
||||
|
||||
9
src/my-type/vscode-globalstorage-json.d.ts
vendored
Normal file
9
src/my-type/vscode-globalstorage-json.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface VSCodeGlobalStorageJson {
|
||||
profileAssociations: {
|
||||
/** 此键值对中项目越靠后,时间越新,我估计是的。<br/>
|
||||
* key:远程开发链接为 `vscode-remote://`,本地文件(目录)为 `file:///`。<br/>
|
||||
* value:难说到底有没有用,`__default__profile__`。
|
||||
*/
|
||||
workspaces: Record<string, string>
|
||||
}
|
||||
}
|
||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import { settingsDto, checkIDEsResultDto } from '@my-type/settings'
|
||||
import { checkIDEsVersionDto } from "../my-type/settings";
|
||||
import { IdeProjectsDto } from "../my-type/ide-projects";
|
||||
|
||||
// 此处只有签名
|
||||
|
||||
@@ -18,6 +19,8 @@ declare global {
|
||||
_checkIDEs: () => Promise<checkIDEsResultDto>
|
||||
_getIDEsVersion: () => Promise<checkIDEsVersionDto>
|
||||
_checkIDEsVersion: () => Promise<checkIDEsVersionDto>
|
||||
_getVSCodeProjects: () => Promise<IdeProjectsDto>
|
||||
_getJetBrainsProjects: () => Promise<IdeProjectsDto>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ const codeLaunchpadApi = {
|
||||
_getIDEs: () => ipcRenderer.invoke('codeLaunchpad:getIDEs'),
|
||||
_checkIDEs: () => ipcRenderer.invoke('codeLaunchpad:checkIDEs'),
|
||||
_getIDEsVersion: () => ipcRenderer.invoke('codeLaunchpad:getIDEsVersion'),
|
||||
_checkIDEsVersion: () => ipcRenderer.invoke('codeLaunchpad:checkIDEsVersion')
|
||||
_checkIDEsVersion: () => ipcRenderer.invoke('codeLaunchpad:checkIDEsVersion'),
|
||||
_getVSCodeProjects: () => ipcRenderer.invoke('codeLaunchpad:getVSCodeProjects'),
|
||||
_getJetBrainsProjects: () => ipcRenderer.invoke('codeLaunchpad:getJetBrainsProjects')
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
Reference in New Issue
Block a user