├── .npmignore ├── docs └── build.md ├── src ├── modules │ ├── demo │ │ ├── index.ts │ │ └── browser │ │ │ ├── index.ts │ │ │ ├── demo.ts │ │ │ ├── editor-empty-component.module.less │ │ │ └── editor-empty-component.contribution.tsx │ ├── topbar │ │ ├── index.ts │ │ ├── node │ │ │ ├── topbar-node-server.ts │ │ │ └── index.ts │ │ ├── common │ │ │ └── index.ts │ │ └── browser │ │ │ ├── topbar.service.ts │ │ │ ├── topbar.view.tsx │ │ │ └── index.ts │ └── basic │ │ └── browser │ │ ├── menu.contribution.ts │ │ ├── index.ts │ │ └── theme.contribution.ts ├── typings │ └── style.d.ts ├── extension │ ├── index.worker.ts │ └── index.ts ├── extensionManager │ ├── node.ts │ └── browser.ts ├── common │ ├── i18n │ │ ├── setup.ts │ │ ├── zh-CN.lang.ts │ │ └── en-US.lang.ts │ ├── types.ts │ ├── constants.ts │ └── node │ │ └── utils.ts ├── browser │ ├── module.ts │ ├── layout.ts │ ├── index.html │ ├── app.tsx │ ├── commands.ts │ ├── project.ts │ └── index.ts ├── main │ ├── services │ │ ├── index.ts │ │ ├── hello.ts │ │ └── storage.ts │ ├── module.ts │ ├── index.ts │ └── launch.ts └── node │ ├── index.ts │ ├── server.ts │ └── module.ts ├── .prettierignore ├── .husky └── pre-commit ├── snapshots └── sumi-electron.png ├── .npmrc ├── .prettierrc ├── product.json ├── .github ├── workflows │ ├── ci.yml │ ├── release.yml │ ├── pack-dmg.yml │ └── codeql-analysis.yml └── renovate.json ├── resources └── darwin │ └── bin │ └── sumi ├── tsconfig.json ├── .vscode └── launch.json ├── README.md ├── LICENSE ├── .gitignore ├── scripts ├── link-local.js ├── apply-product.js ├── rebuild-native.js └── download-extensions.js ├── .eslintrc.js └── package.json /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | vendor 3 | scripts 4 | -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | ## product.json 4 | -------------------------------------------------------------------------------- /src/modules/demo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | app 2 | out 3 | extensions 4 | node_modules 5 | -------------------------------------------------------------------------------- /src/typings/style.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less'; 2 | declare module '*.css'; 3 | -------------------------------------------------------------------------------- /src/modules/topbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './node'; 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /src/extension/index.worker.ts: -------------------------------------------------------------------------------- 1 | import '@opensumi/ide-extension/lib/hosted/worker.host-preload'; 2 | -------------------------------------------------------------------------------- /snapshots/sumi-electron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensumi/ide-electron/HEAD/snapshots/sumi-electron.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # See: https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 2 | shamefully-hoist = true -------------------------------------------------------------------------------- /src/extensionManager/node.ts: -------------------------------------------------------------------------------- 1 | export { OpenVsxExtensionManagerModule as ExtensionManagerModule } from '@opensumi/ide-extension-manager/lib/node'; 2 | -------------------------------------------------------------------------------- /src/extensionManager/browser.ts: -------------------------------------------------------------------------------- 1 | export { OpenVsxExtensionManagerModule as ExtensionManagerModule } from '@opensumi/ide-extension-manager/lib/browser'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 120, 6 | "proseWrap": "never", 7 | "endOfLine": "lf" 8 | } 9 | -------------------------------------------------------------------------------- /product.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "OpenSumi-OSS", 3 | "applicationName": "sumi-oss", 4 | "dataFolderName": ".sumi-oss", 5 | "version": "1.3.6", 6 | "urlProtocol": "sumi-oss", 7 | "serverApp": { "marketplace": {} }, 8 | "devtoolFrontendUrl": "", 9 | "sumiVersion": "" 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/topbar/node/topbar-node-server.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@opensumi/di'; 2 | import { ITopbarNodeServer } from '../common'; 3 | 4 | @Injectable() 5 | export class TopbarNodeServer implements ITopbarNodeServer { 6 | topbarHello() { 7 | console.log('you click topbar'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/i18n/setup.ts: -------------------------------------------------------------------------------- 1 | import { registerLocalizationBundle } from '@opensumi/ide-core-common/lib/localize'; 2 | import { localizationBundle as zh } from './zh-CN.lang'; 3 | import { localizationBundle as en } from './en-US.lang'; 4 | 5 | // 先初始化语言包 6 | registerLocalizationBundle(zh); 7 | registerLocalizationBundle(en); 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | ci: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Use Node.js 14.x 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 14.x 15 | 16 | - name: Run CI 17 | run: | 18 | yarn install 19 | yarn build 20 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "timezone": "Asia/Shanghai", 5 | "enabledManagers": ["npm"], 6 | "groupName": "opensumi packages", 7 | "packageRules": [ 8 | { 9 | "packagePatterns": ["*"], 10 | "excludePackagePatterns": ["^@opensumi/ide-"], 11 | "enabled": false 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/common/i18n/zh-CN.lang.ts: -------------------------------------------------------------------------------- 1 | export const localizationBundle = { 2 | languageId: 'zh-CN', 3 | languageName: 'Chinese', 4 | localizedLanguageName: '中文(中国)', 5 | contents: { 6 | 'common.about': '关于', 7 | 'common.preferences': '首选项', 8 | 9 | 'custom.quick_open': '转到文件', 10 | 'custom.command_palette': '显示所有命令', 11 | 'custom.terminal_panel': '切换终端', 12 | 'custom.search_panel': '切换搜索面板', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/modules/topbar/common/index.ts: -------------------------------------------------------------------------------- 1 | export class CommonCls { 2 | add(a: number, b: number) { 3 | return a + b; 4 | } 5 | } 6 | export const ITopbarNodeServer = 'ITopbarNodeServer'; 7 | export const TopbarNodeServerPath = 'TopbarNodeServerPath'; 8 | export interface ITopbarNodeServer { 9 | topbarHello: () => void; 10 | } 11 | 12 | export const ITopbarService = 'ITopbarService'; 13 | export interface ITopbarService { 14 | sayHelloFromNode: () => void; 15 | } 16 | -------------------------------------------------------------------------------- /src/common/i18n/en-US.lang.ts: -------------------------------------------------------------------------------- 1 | export const localizationBundle = { 2 | languageId: 'en-US', 3 | languageName: 'English', 4 | localizedLanguageName: 'English', 5 | contents: { 6 | 'common.about': 'About', 7 | 'common.preferences': 'Preferences', 8 | 9 | 'custom.quick_open': 'Quick Open', 10 | 'custom.command_palette': 'Command Palette', 11 | 'custom.terminal_panel': 'Switch to Terminal Panel', 12 | 'custom.search_panel': 'Switch to Search Panel', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/extension/index.ts: -------------------------------------------------------------------------------- 1 | import { extProcessInit } from '@opensumi/ide-extension/lib/hosted/ext.process-base.js'; 2 | import { ExtensionCommands } from 'common/constants'; 3 | import { openNodeDevtool } from 'common/node/utils'; 4 | 5 | (async () => { 6 | await extProcessInit({ 7 | builtinCommands: [ 8 | { 9 | id: ExtensionCommands.OPEN_DEVTOOLS, 10 | handler: { 11 | handler: () => { 12 | openNodeDevtool(); 13 | }, 14 | }, 15 | }, 16 | ], 17 | }); 18 | })(); 19 | -------------------------------------------------------------------------------- /src/modules/topbar/browser/topbar.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Autowired } from '@opensumi/di'; 2 | import { Disposable } from '@opensumi/ide-core-common'; 3 | import { ITopbarNodeServer, ITopbarService, TopbarNodeServerPath } from '../common'; 4 | 5 | @Injectable() 6 | export class TopbarService extends Disposable implements ITopbarService { 7 | @Autowired(TopbarNodeServerPath) 8 | topbarNodeServer: ITopbarNodeServer; 9 | 10 | sayHelloFromNode() { 11 | console.log('browser hello!'); 12 | this.topbarNodeServer.topbarHello(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/browser/module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@opensumi/ide-core-browser'; 2 | import { Injectable } from '@opensumi/di'; 3 | 4 | import { ProjectSwitcherContribution } from './project'; 5 | import { MainCommandContribution } from './commands'; 6 | import { Constants } from '../common/constants'; 7 | 8 | @Injectable() 9 | export class MiniDesktopModule extends BrowserModule { 10 | providers = [MainCommandContribution, ProjectSwitcherContribution]; 11 | 12 | backServices = [ 13 | { 14 | servicePath: Constants.ELECTRON_NODE_SERVICE_PATH, 15 | }, 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/topbar/node/index.ts: -------------------------------------------------------------------------------- 1 | import { Provider, Injectable } from '@opensumi/di'; 2 | import { NodeModule } from '@opensumi/ide-core-node'; 3 | import { ITopbarNodeServer, TopbarNodeServerPath } from '../common'; 4 | import { TopbarNodeServer } from './topbar-node-server'; 5 | 6 | @Injectable() 7 | export class TopBarModule extends NodeModule { 8 | providers: Provider[] = [ 9 | { 10 | token: ITopbarNodeServer, 11 | useClass: TopbarNodeServer, 12 | }, 13 | ]; 14 | 15 | backServices = [ 16 | { 17 | token: ITopbarNodeServer, 18 | servicePath: TopbarNodeServerPath, 19 | }, 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/basic/browser/menu.contribution.ts: -------------------------------------------------------------------------------- 1 | import { IMenuRegistry, MenuId } from "@opensumi/ide-core-browser/lib/menu/next"; 2 | import { localize } from "@opensumi/ide-core-common/lib/localize"; 3 | import { ElectronBasicContribution } from "@opensumi/ide-electron-basic/lib/browser"; 4 | 5 | export class LocalMenuContribution extends ElectronBasicContribution { 6 | 7 | registerMenus(menuRegistry: IMenuRegistry) { 8 | menuRegistry.registerMenuItem(MenuId.MenubarAppMenu, { 9 | submenu: MenuId.SettingsIconMenu, 10 | label: localize('common.preferences'), 11 | group: '2_preference', 12 | }); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { IStorage } from '@opensumi/ide-core-common'; 2 | 3 | export const IHelloService = 'IHelloService'; 4 | export interface IHelloService { 5 | hello(): Promise; 6 | } 7 | 8 | export const IMainStorageService = 'IMainStorageService'; 9 | export interface IMainStorageService { 10 | homeDir: string; 11 | setRootStoragePath: (storagePath: string) => void; 12 | getStoragePath: (storageName: string) => Promise; 13 | getItem: (storageName: string) => Promise; 14 | getItemSync: (storageName: string) => T; 15 | setItem: (storageName: string, value: any) => Promise; 16 | } 17 | -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const Constants = { 2 | ELECTRON_MAIN_API_NAME: 'opensumi-main-api', 3 | ELECTRON_NODE_SERVICE_NAME: 'opensumi-electron-node', 4 | ELECTRON_NODE_SERVICE_PATH: 'opensumi-electron-node-service-path', 5 | 6 | DATA_FOLDER: process.env.DATA_FOLDER || '.sumi', 7 | 8 | DEFAULT_BACKGROUND: 'rgb(32, 34, 36)', 9 | }; 10 | 11 | export const Commands = { 12 | OPEN_DEVTOOLS_MAIN: 'opensumi.help.openDevtools.main', 13 | OPEN_DEVTOOLS_NODE: 'opensumi.help.openDevtools.node', 14 | OPEN_DEVTOOLS_EXTENSION: 'opensumi.help.openDevtools.extension', 15 | }; 16 | 17 | export const ExtensionCommands = { 18 | OPEN_DEVTOOLS: 'extension.opensumi.openDevtools', 19 | }; 20 | -------------------------------------------------------------------------------- /src/modules/basic/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@opensumi/di'; 2 | import { createElectronMainApi } from '@opensumi/ide-core-browser'; 3 | import { ElectronBasicModule } from '@opensumi/ide-electron-basic/lib/browser'; 4 | import { IMainStorageService } from 'common/types'; 5 | import { LocalMenuContribution } from './menu.contribution'; 6 | import { LocalThemeContribution } from './theme.contribution'; 7 | 8 | @Injectable() 9 | export class LocalBasicModule extends ElectronBasicModule { 10 | providers = [ 11 | LocalMenuContribution, 12 | LocalThemeContribution, 13 | { 14 | token: IMainStorageService, 15 | useValue: createElectronMainApi(IMainStorageService), 16 | }, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/demo/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@opensumi/di'; 2 | import { createElectronMainApi } from '@opensumi/ide-core-browser'; 3 | import { ElectronBasicModule } from '@opensumi/ide-electron-basic/lib/browser'; 4 | import { IHelloService } from '../../../common/types'; 5 | import { DemoContribution } from './demo'; 6 | import { EditorEmptyComponentContribution } from './editor-empty-component.contribution'; 7 | 8 | @Injectable() 9 | export class DemoModule extends ElectronBasicModule { 10 | providers = [ 11 | { 12 | token: IHelloService, 13 | useValue: createElectronMainApi(IHelloService), 14 | }, 15 | DemoContribution, 16 | EditorEmptyComponentContribution, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Autowired, Injectable, Injector, INJECTOR_TOKEN } from '@opensumi/di'; 2 | import { ElectronMainModule } from '@opensumi/ide-core-electron-main/lib/electron-main-module'; 3 | import { IHelloService, IMainStorageService } from '../../common/types'; 4 | import { HelloContribution, HelloService } from './hello'; 5 | import { MainStorageContribution, MainStorageService } from './storage'; 6 | 7 | @Injectable() 8 | export class MainModule extends ElectronMainModule { 9 | providers = [ 10 | { 11 | token: IHelloService, 12 | useClass: HelloService, 13 | }, 14 | HelloContribution, 15 | { 16 | token: IMainStorageService, 17 | useClass: MainStorageService, 18 | }, 19 | MainStorageContribution, 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/topbar/browser/topbar.view.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { useInjectable, ComponentRegistryInfo } from '@opensumi/ide-core-browser'; 4 | import { Button, Badge } from '@opensumi/ide-components'; 5 | import { ITopbarService } from '../common'; 6 | 7 | export const TopbarBadge: React.FC<{ component: ComponentRegistryInfo }> = observer(({ component }) => (component.options!.badge && ) || null); 8 | 9 | export const Topbar = observer(() => { 10 | const topbarService = useInjectable(ITopbarService); 11 | 12 | const onClick = () => { 13 | topbarService.sayHelloFromNode(); 14 | }; 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /resources/darwin/bin/sumi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See License.txt in the project root for license information. 5 | 6 | function app_realpath() { 7 | SOURCE=$1 8 | while [ -h "$SOURCE" ]; do 9 | DIR=$(dirname "$SOURCE") 10 | SOURCE=$(readlink "$SOURCE") 11 | [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE 12 | done 13 | SOURCE_DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" 14 | echo "${SOURCE_DIR%%${SOURCE_DIR#*.app}}" 15 | } 16 | 17 | APP_PATH="$(app_realpath "${BASH_SOURCE[0]}")" 18 | if [ -z "$APP_PATH" ]; then 19 | echo "Unable to determine app path from symlink : ${BASH_SOURCE[0]}" 20 | exit 1 21 | fi 22 | CONTENTS="$APP_PATH/Contents" 23 | ELECTRON="$CONTENTS/MacOS/OpenSumi" 24 | CLI="$CONTENTS/Resources/app.asar" 25 | "$ELECTRON" "$CLI" "$@" 26 | exit $? 27 | -------------------------------------------------------------------------------- /src/modules/demo/browser/demo.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di'; 2 | import { ClientAppContribution } from '@opensumi/ide-core-browser'; 3 | import { Domain } from '@opensumi/ide-core-common'; 4 | import { IElectronMainApi } from '@opensumi/ide-core-common/lib/electron'; 5 | import { IHelloService } from '../../../common/types'; 6 | 7 | interface IHelloMainService extends IElectronMainApi, IHelloService {} 8 | 9 | @Domain(ClientAppContribution) 10 | export class DemoContribution implements ClientAppContribution { 11 | @Autowired(IHelloService) 12 | helloService: IHelloMainService; 13 | 14 | initialize() { 15 | this.helloService.on('hello-event', (payload) => { 16 | console.log('Got payload from Main Process:', payload); 17 | }); 18 | 19 | // Demo 20 | setTimeout(() => { 21 | this.helloService.hello(); 22 | }, 2000); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/demo/browser/editor-empty-component.module.less: -------------------------------------------------------------------------------- 1 | .empty_component { 2 | display: flex; 3 | height: 100%; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | img { 8 | width: 100px; 9 | height: 100px; 10 | margin-bottom: 20px; 11 | user-select: none; 12 | } 13 | > span { 14 | font-size: 20px; 15 | opacity: 0.7; 16 | } 17 | } 18 | 19 | .shortcutPanel { 20 | border-collapse: separate; 21 | border-spacing: 13px 10px; 22 | user-select: none; 23 | 24 | .shortcutRow { 25 | display: table-row; 26 | opacity: 0.7; 27 | padding-top: 8px; 28 | padding-right: 16px; 29 | 30 | .label { 31 | // width: 92px; 32 | display: table-cell; 33 | text-align: right; 34 | } 35 | 36 | .keybinding { 37 | display: table-cell; 38 | margin-left: 16px; 39 | text-align: left; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "jsx": "react", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "baseUrl": "src", 9 | "strict": true, 10 | "strictPropertyInitialization": false, 11 | "declaration": true, 12 | "sourceMap": true, 13 | "removeComments": true, 14 | "emitDecoratorMetadata": true, 15 | "experimentalDecorators": true, 16 | "importHelpers": true, 17 | "resolveJsonModule": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "downlevelIteration": true, 21 | "noEmitOnError": false, 22 | "noImplicitAny": false, 23 | "skipLibCheck": true, 24 | "useUnknownInCatchVariables": false, 25 | "isolatedModules": true, 26 | "outDir": "app", 27 | "rootDir": "src", 28 | "lib": ["dom", "es2019"] 29 | }, 30 | "include": ["src/**/*"] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "pack", 11 | "program": "${workspaceFolder}/build/pack/index.js" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "attach", 16 | "name": "Attach to Extension Host", 17 | "port": 9889, 18 | "restart": true 19 | }, 20 | { 21 | "name": "Debug Main Process", 22 | "type": "node", 23 | "request": "launch", 24 | "cwd": "${workspaceRoot}", 25 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 26 | "windows": { 27 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 28 | }, 29 | "args": ["."], 30 | "outputCapture": "std" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/main/services/hello.ts: -------------------------------------------------------------------------------- 1 | import { Autowired, Injectable, Injector, INJECTOR_TOKEN } from '@opensumi/di'; 2 | import { Domain } from '@opensumi/ide-core-common'; 3 | import { 4 | ElectronMainApiProvider, 5 | ElectronMainApiRegistry, 6 | ElectronMainContribution, 7 | } from '@opensumi/ide-core-electron-main/lib/bootstrap/types'; 8 | import { IHelloService } from '../../common/types'; 9 | 10 | @Injectable() 11 | export class HelloService extends ElectronMainApiProvider implements IHelloService { 12 | async hello() { 13 | this.eventEmitter.fire('hello-event', { 14 | content: 'from main process.', 15 | }); 16 | } 17 | } 18 | 19 | @Domain(ElectronMainContribution) 20 | export class HelloContribution implements ElectronMainContribution { 21 | @Autowired(INJECTOR_TOKEN) 22 | injector: Injector; 23 | 24 | registerMainApi(registry: ElectronMainApiRegistry) { 25 | // registry.registerMainApi(IHelloService, this.injector.get(IHelloService)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenSumi IDE Electron 2 | 3 | English | [中文文档](https://opensumi.com/zh/docs/integrate/quick-start/electron) 4 | 5 | ![OpenSumi Desktop](./snapshots/sumi-electron.png) 6 | 7 | ## Startup 8 | 9 | ```shell 10 | git clone git@github.com:opensumi/ide-electron.git 11 | cd ide-electron 12 | yarn 13 | yarn build 14 | yarn rebuild-native --force-rebuild=true 15 | yarn download-extension # install extension (Optional) 16 | yarn start 17 | ``` 18 | 19 | to use the China CDN mirror, checkout branch `main-cn`: 20 | 21 | ```shell 22 | git checkout main-cn 23 | ``` 24 | 25 | ## Develop 26 | 27 | Start application: 28 | 29 | ```shell 30 | yarn watch 31 | yarn start 32 | ``` 33 | 34 | When there are new changes in the code, open the command panel in the editor shift+command+p, select and run the 'Reload Window' to reload the current editor window. 35 | 36 | ## package to DMG 37 | 38 | package the project, and the installation package in the `out` directory: 39 | 40 | ```shell 41 | yarn run pack 42 | ``` 43 | -------------------------------------------------------------------------------- /src/common/node/utils.ts: -------------------------------------------------------------------------------- 1 | import * as inspector from 'inspector'; 2 | 3 | function isOpen() { 4 | return !!inspector.url(); 5 | } 6 | 7 | export function getInspectWsPath() { 8 | if (!isOpen()) { 9 | const port = 30000 + Math.floor(Math.random() * 10000); 10 | inspector.open(port); 11 | } 12 | 13 | const url = inspector.url(); 14 | return url; 15 | } 16 | 17 | export function openNodeDevtool() { 18 | const url = getInspectWsPath(); 19 | if (!url) { 20 | return; 21 | } 22 | 23 | let devtoolUrl = 'chrome://inspect/'; 24 | 25 | if (process.env.DEVTOOL_FRONTEND_URL) { 26 | // remove first 5 char `ws://xxx` -> `xxx` 27 | devtoolUrl = process.env.DEVTOOL_FRONTEND_URL + url.substring(5); 28 | } 29 | 30 | if (!devtoolUrl) { 31 | return; 32 | } 33 | 34 | const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; 35 | console.log('you can open the devtool url', devtoolUrl); 36 | require('child_process').exec(start + ' ' + devtoolUrl); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElectronMainApiProvider, 3 | ElectronMainApiRegistry, 4 | ElectronMainContribution, 5 | ElectronMainModule, 6 | } from '@opensumi/ide-core-electron-main'; 7 | import { Domain } from '@opensumi/ide-core-common'; 8 | import { Injectable, Autowired } from '@opensumi/di'; 9 | import { openNodeDevtool } from '../common/node/utils'; 10 | import { Constants } from '../common/constants'; 11 | 12 | @Injectable() 13 | export class OpenSumiDesktopMainModule extends ElectronMainModule { 14 | providers = [OpenSumiDesktopMainContribution]; 15 | } 16 | 17 | @Injectable() 18 | export class OpenSumiDesktopMainApi extends ElectronMainApiProvider { 19 | debugMain() { 20 | openNodeDevtool(); 21 | } 22 | } 23 | 24 | @Domain(ElectronMainContribution) 25 | export class OpenSumiDesktopMainContribution implements ElectronMainContribution { 26 | @Autowired() 27 | api: OpenSumiDesktopMainApi; 28 | 29 | registerMainApi(registry: ElectronMainApiRegistry) { 30 | registry.registerMainApi(Constants.ELECTRON_MAIN_API_NAME, this.api); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/browser/layout.ts: -------------------------------------------------------------------------------- 1 | import { LayoutConfig, SlotLocation } from '@opensumi/ide-core-browser'; 2 | 3 | export const customLayoutConfig: LayoutConfig = { 4 | [SlotLocation.top]: { 5 | modules: ['@opensumi/ide-menu-bar', 'topbar', 'toolbar'], 6 | }, 7 | [SlotLocation.action]: { 8 | modules: ['@opensumi/ide-toolbar-action'], 9 | }, 10 | [SlotLocation.left]: { 11 | modules: [ 12 | '@opensumi/ide-explorer', 13 | '@opensumi/ide-search', 14 | '@opensumi/ide-scm', 15 | '@opensumi/ide-extension-manager', 16 | '@opensumi/ide-debug', 17 | ], 18 | }, 19 | [SlotLocation.right]: { 20 | modules: [], 21 | }, 22 | [SlotLocation.main]: { 23 | modules: ['@opensumi/ide-editor'], 24 | }, 25 | [SlotLocation.bottom]: { 26 | modules: [ 27 | '@opensumi/ide-terminal-next', 28 | '@opensumi/ide-output', 29 | 'debug-console', 30 | '@opensumi/ide-markers', 31 | '@opensumi/ide-refactor-preview', 32 | ], 33 | }, 34 | [SlotLocation.statusBar]: { 35 | modules: ['@opensumi/ide-status-bar'], 36 | }, 37 | [SlotLocation.extra]: { 38 | modules: ['breadcrumb-menu'], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Alibaba Group Holding Limited, Ant Technology Group. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/modules/topbar/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { Provider, Injectable } from '@opensumi/di'; 2 | import { BrowserModule, Domain } from '@opensumi/ide-core-browser'; 3 | import { ComponentContribution, ComponentRegistry } from '@opensumi/ide-core-browser/lib/layout'; 4 | import { ITopbarService, TopbarNodeServerPath } from '../common'; 5 | import { TopbarService } from './topbar.service'; 6 | import { Topbar } from './topbar.view'; 7 | 8 | @Injectable() 9 | export class TopbarModule extends BrowserModule { 10 | providers: Provider[] = [ 11 | TopbarContribution, 12 | { 13 | token: ITopbarService, 14 | useClass: TopbarService, 15 | }, 16 | ]; 17 | 18 | backServices = [ 19 | { 20 | servicePath: TopbarNodeServerPath, 21 | }, 22 | ]; 23 | 24 | component = Topbar; 25 | } 26 | 27 | @Domain(ComponentContribution) 28 | export class TopbarContribution implements ComponentContribution { 29 | registerComponent(registry: ComponentRegistry): void { 30 | registry.register( 31 | 'topbar', 32 | { 33 | id: 'topbar', 34 | component: Topbar, 35 | }, 36 | { 37 | size: 56, 38 | }, 39 | ); 40 | } 41 | 42 | registerRenderer() {} 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Pre release by tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-latest 12 | 13 | strategy: 14 | matrix: 15 | node_version: [14.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Package Electron 26 | run: | 27 | node ./scripts/apply-product.js 28 | echo "package.json:" && cat package.json 29 | echo "./build/package.json" && cat ./build/package.json 30 | yarn 31 | yarn download-extension 32 | yarn pack 33 | env: 34 | TARGET_PLATFORMS: darwin 35 | TARGET_ARCHES: x64,arm64 36 | PRODUCT_VERSION: ${{ github.ref_name }} 37 | 38 | - uses: 'marvinpinto/action-automatic-releases@latest' 39 | with: 40 | repo_token: '${{ secrets.GITHUB_TOKEN }}' 41 | prerelease: true 42 | files: | 43 | out/LICENSE 44 | out/*.dmg 45 | out/*.exe 46 | -------------------------------------------------------------------------------- /src/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | Sumi 17 | 49 | 50 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/node/index.ts: -------------------------------------------------------------------------------- 1 | import { startServer } from './server'; 2 | 3 | import { NodeModule, ConstructorOf } from '@opensumi/ide-core-node'; 4 | import { ServerCommonModule } from '@opensumi/ide-core-node'; 5 | import { FileServiceModule } from '@opensumi/ide-file-service/lib/node'; 6 | 7 | import { ProcessModule } from '@opensumi/ide-process/lib/node'; 8 | 9 | import { FileSearchModule } from '@opensumi/ide-file-search/lib/node'; 10 | import { SearchModule } from '@opensumi/ide-search/lib/node'; 11 | import { TerminalNodePtyModule } from '@opensumi/ide-terminal-next/lib/node'; 12 | import { LogServiceModule } from '@opensumi/ide-logs/lib/node'; 13 | import { ExtensionModule } from '@opensumi/ide-extension/lib/node'; 14 | import { FileSchemeNodeModule } from '@opensumi/ide-file-scheme/lib/node'; 15 | import { AddonsModule } from '@opensumi/ide-addons/lib/node'; 16 | import { MiniCodeDesktopNodeModule } from './module'; 17 | 18 | import { ExtensionManagerModule } from '../extensionManager/node'; 19 | 20 | export const CommonNodeModules: ConstructorOf[] = [ 21 | ServerCommonModule, 22 | LogServiceModule, 23 | FileServiceModule, 24 | ProcessModule, 25 | FileSearchModule, 26 | SearchModule, 27 | TerminalNodePtyModule, 28 | ExtensionModule, 29 | ExtensionManagerModule, 30 | FileSchemeNodeModule, 31 | AddonsModule, 32 | ]; 33 | 34 | startServer({ 35 | modules: [...CommonNodeModules, MiniCodeDesktopNodeModule], 36 | }).then(() => { 37 | console.log('ready'); 38 | if (process.send) { 39 | process.send('ready'); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app } from 'electron'; 3 | import { launch } from './launch'; 4 | import minimist from 'minimist'; 5 | import { existsSync } from 'fs-extra'; 6 | 7 | const launchFromCommandLine = (processArgv: string[], workingDirectory: string): Promise => { 8 | console.log('processArgv', processArgv); 9 | 10 | const parsedArgs = minimist(processArgv); 11 | const _argv = parsedArgs['_'].map((arg) => String(arg)).filter((arg) => arg.length > 0); 12 | const [, , ...argv] = _argv; // remove the first non-option argument: it's always the app location 13 | 14 | console.log('🚀 ~ file: index.ts ~ line 17 ~ launchFromCommandLine ~ argv', argv); 15 | console.log('working directory', workingDirectory); 16 | 17 | if (argv.length === 0) { 18 | return launch(); 19 | } 20 | 21 | try { 22 | const argvPath = path.resolve(argv[0]); 23 | const exists = existsSync(argvPath); 24 | if (exists) { 25 | return launch(argvPath); 26 | } 27 | 28 | const workspace = path.resolve(workingDirectory, argv[0]); 29 | return launch(workspace); 30 | } catch (e) { 31 | console.error('parse argv error', e); 32 | return launch(); 33 | } 34 | }; 35 | 36 | const isSingleInstance = app.requestSingleInstanceLock(); 37 | if (!isSingleInstance) { 38 | app.quit(); 39 | process.exit(0); 40 | } 41 | 42 | app.on('second-instance', (event, commandLine, workingDirectory) => { 43 | launchFromCommandLine(commandLine, workingDirectory).catch(console.error); 44 | }); 45 | 46 | launchFromCommandLine(process.argv, process.cwd()).catch(console.error); 47 | -------------------------------------------------------------------------------- /src/browser/app.tsx: -------------------------------------------------------------------------------- 1 | import { IClientAppOpts, electronEnv, URI } from '@opensumi/ide-core-browser'; 2 | import { Injector } from '@opensumi/di'; 3 | import { ClientApp } from '@opensumi/ide-core-browser/lib/bootstrap/app'; 4 | 5 | // 引入公共样式文件 6 | import '@opensumi/ide-core-browser/lib/style/index.less'; 7 | // 引入本地icon,不使用cdn版本,与useCdnIcon配套使用 8 | import '@opensumi/ide-core-browser/lib/style/icon.less'; 9 | import { IElectronMainLifeCycleService } from '@opensumi/ide-core-common/lib/electron'; 10 | import { Constants } from 'common/constants'; 11 | import 'common/i18n/setup'; 12 | 13 | export async function renderApp(opts: IClientAppOpts) { 14 | const injector = new Injector(); 15 | 16 | opts.workspaceDir = electronEnv.env.WORKSPACE_DIR; 17 | opts.extensionDir = electronEnv.metadata.extensionDir; 18 | opts.injector = injector; 19 | 20 | opts.preferenceDirName = Constants.DATA_FOLDER; 21 | opts.storageDirName = Constants.DATA_FOLDER; 22 | opts.extensionStorageDirName = Constants.DATA_FOLDER; 23 | 24 | if (electronEnv.metadata.workerHostEntry) { 25 | opts.extWorkerHost = URI.file(electronEnv.metadata.workerHostEntry).toString(); 26 | } 27 | opts.didRendered = () => { 28 | const loadingDom = document.getElementById('loading'); 29 | if (loadingDom) { 30 | loadingDom.classList.add('loading-hidden'); 31 | loadingDom.remove(); 32 | } 33 | }; 34 | 35 | const app = new ClientApp(opts); 36 | 37 | // 拦截reload行为 38 | app.fireOnReload = () => { 39 | injector.get(IElectronMainLifeCycleService).reloadWindow(electronEnv.currentWindowId); 40 | }; 41 | 42 | app.start(document.getElementById('main')!, 'electron'); 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 由 https://github.com/msfeldstein/gitignore 自动生成 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | # typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | dist 82 | lib 83 | out 84 | .vscode/* 85 | !.vscode/launch.json 86 | 87 | tools/workspace/* 88 | 89 | packages/feature-extension/test/init/node_modules 90 | packages/vscode-extension/test/init/node_modules 91 | 92 | extensions 93 | app 94 | 95 | out-x64 96 | out-arm64 97 | 98 | .DS_Store 99 | .idea 100 | -------------------------------------------------------------------------------- /scripts/link-local.js: -------------------------------------------------------------------------------- 1 | const { resolve, basename, join, dirname } = require('path'); 2 | const { execSync } = require('child_process'); 3 | const { existsSync, symlinkSync, mkdirSync, readdirSync, readFileSync, unlinkSync } = require('fs'); 4 | 5 | const KTFrameworkDir = process.env['IDE_FRAMEWORK_PATH'] || resolve('../ide-framework/'); 6 | 7 | function ensureDirSync(path) { 8 | if (!existsSync(path)) { 9 | mkdirSync(path); 10 | } 11 | } 12 | 13 | function linkNodeModules(moduleDir, targetParent) { 14 | let name = basename(moduleDir); 15 | if (name.startsWith('_')) { 16 | return; 17 | } 18 | if (name.startsWith('@') || name === 'node_modules') { 19 | const target = join(targetParent, name); 20 | ensureDirSync(target); 21 | readdirSync(moduleDir).forEach((dir) => { 22 | linkNodeModules(join(moduleDir, dir), target); 23 | }); 24 | } else { 25 | const target = join(targetParent, name); 26 | if (!existsSync(target)) { 27 | try { 28 | unlinkSync(target); // 失效link 29 | } catch (e) { 30 | // ignore 31 | } 32 | 33 | symlinkSync(moduleDir, target); 34 | } 35 | } 36 | } 37 | 38 | linkNodeModules(join(KTFrameworkDir, '/node_modules'), resolve('./')); 39 | 40 | function linkPackages(packagesDir, targetParent) { 41 | readdirSync(packagesDir).forEach((packageName) => { 42 | if (packageName.startsWith('.')) { 43 | return; 44 | } 45 | if (!existsSync(join(packagesDir, packageName, 'package.json'))) { 46 | return; 47 | } 48 | const name = JSON.parse(readFileSync(join(packagesDir, packageName, 'package.json'), 'utf8').toString()).name; 49 | const target = join(targetParent, name); 50 | try { 51 | unlinkSync(target); // 失效link 52 | } catch (e) { 53 | // ignore 54 | } 55 | ensureDirSync(dirname(target)); 56 | symlinkSync(join(packagesDir, packageName), target); 57 | }); 58 | } 59 | 60 | linkPackages(join(KTFrameworkDir, '/packages'), resolve('./node_modules')); 61 | -------------------------------------------------------------------------------- /src/node/server.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net'; 2 | import * as yargs from 'yargs'; 3 | import path from 'path'; 4 | import { homedir } from 'os'; 5 | import { Deferred } from '@opensumi/ide-core-common'; 6 | import { IServerAppOpts, ServerApp } from '@opensumi/ide-core-node'; 7 | import { Constants } from '../common/constants'; 8 | 9 | declare const SERVER_APP_OPTS: Record & { 10 | marketplace: Record; 11 | }; 12 | 13 | function getDefinedServerOpts() { 14 | try { 15 | return SERVER_APP_OPTS; 16 | } catch { 17 | return { 18 | marketplace: {}, 19 | }; 20 | } 21 | } 22 | 23 | function getDataFolder(sub: string) { 24 | return path.join(homedir(), Constants.DATA_FOLDER, sub); 25 | } 26 | 27 | function getServerAppOpts() { 28 | let opts: IServerAppOpts = { 29 | webSocketHandler: [], 30 | marketplace: { 31 | showBuiltinExtensions: true, 32 | extensionDir: getDataFolder('extensions'), 33 | }, 34 | logDir: getDataFolder('logs'), 35 | }; 36 | try { 37 | const newOpts = getDefinedServerOpts(); 38 | 39 | opts = { 40 | ...opts, 41 | ...newOpts, 42 | marketplace: { 43 | showBuiltinExtensions: true, 44 | ...newOpts.marketplace, 45 | ...opts.marketplace, 46 | }, 47 | }; 48 | } catch (error) {} 49 | return opts; 50 | } 51 | 52 | export async function startServer(_opts: Partial) { 53 | const deferred = new Deferred(); 54 | let opts: IServerAppOpts = getServerAppOpts(); 55 | 56 | opts = { 57 | ...opts, 58 | ..._opts, 59 | }; 60 | 61 | const server = net.createServer(); 62 | const listenPath = (await yargs.argv).listenPath; 63 | console.log('listenPath', listenPath); 64 | 65 | const serverApp = new ServerApp(opts); 66 | 67 | await serverApp.start(server); 68 | 69 | server.on('error', (err) => { 70 | deferred.reject(err); 71 | console.error('server error: ' + err.message); 72 | setTimeout(process.exit, 0, 1); 73 | }); 74 | 75 | server.listen(listenPath, () => { 76 | console.log(`server listen on path ${listenPath}`); 77 | deferred.resolve(server); 78 | }); 79 | 80 | await deferred.promise; 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/basic/browser/theme.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di'; 2 | import { ClientAppContribution } from '@opensumi/ide-core-browser/lib/common'; 3 | import { Domain, IEventBus, ThrottledDelayer } from '@opensumi/ide-core-common'; 4 | import { ThemeChangedEvent } from '@opensumi/ide-theme/lib/common'; 5 | import { IMainStorageService } from 'common/types'; 6 | 7 | export interface ThemeData { 8 | menuBarBackground?: string; 9 | sideBarBackground?: string; 10 | editorBackground?: string; 11 | panelBackground?: string; 12 | statusBarBackground?: string; 13 | } 14 | 15 | const THEME_TIMEOUT = 1000; 16 | 17 | @Domain(ClientAppContribution) 18 | export class LocalThemeContribution implements ClientAppContribution { 19 | @Autowired(IMainStorageService) 20 | private readonly mainStorageService: IMainStorageService; 21 | 22 | @Autowired(IEventBus) 23 | private readonly eventBus: IEventBus; 24 | 25 | private trigger = new ThrottledDelayer(THEME_TIMEOUT); 26 | 27 | initialize() { 28 | this.updateTheme(); 29 | 30 | this.eventBus.on(ThemeChangedEvent, () => { 31 | // e.payload.theme 数据有误 32 | // const theme = e.payload.theme; 33 | this.trigger.trigger(async () => { 34 | this.updateTheme(); 35 | }); 36 | }); 37 | } 38 | 39 | async updateTheme() { 40 | const theme = localStorage.getItem('theme'); 41 | if (!theme) { 42 | return; 43 | } 44 | let mainThemeData: ThemeData | null = null; 45 | try { 46 | mainThemeData = await this.mainStorageService.getItem('theme'); 47 | } catch (error) { 48 | console.error('error:', error); 49 | } 50 | 51 | try { 52 | const themeObj = JSON.parse(theme); 53 | 54 | if (themeObj.editorBackground && mainThemeData && mainThemeData.editorBackground !== themeObj.editorBackground) { 55 | this.mainStorageService.setItem('theme', { 56 | menuBarBackground: themeObj.menuBarBackground, 57 | sideBarBackground: themeObj.sideBarBackground, 58 | editorBackground: themeObj.editorBackground, 59 | panelBackground: themeObj.panelBackground, 60 | statusBarBackground: themeObj.statusBarBackground, 61 | }); 62 | } 63 | } catch (e) { 64 | // do nothing 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/pack-dmg.yml: -------------------------------------------------------------------------------- 1 | name: Pack dmg 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | sumiVersion: 7 | description: "A valid sumi version, fallback use the value defined in `product.json`" 8 | required: false 9 | productVersion: 10 | description: "product version, fallback use the value defined in `product.json`" 11 | required: false 12 | targetArches: 13 | description: "target arches, use comma to split multi arches" 14 | required: true 15 | default: "x64,arm64" 16 | targetPlatforms: 17 | description: "target platforms, use comma to split multi arches" 18 | required: true 19 | default: "darwin" 20 | 21 | jobs: 22 | next-version: 23 | name: Pack 24 | 25 | runs-on: macos-latest 26 | 27 | strategy: 28 | matrix: 29 | node-version: [14.x] 30 | 31 | steps: 32 | # 判断用户是否有写权限 33 | - name: "Check if user has write access" 34 | uses: "lannonbr/repo-permission-check-action@2.0.0" 35 | with: 36 | permission: "write" 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | # Checkout 到 41 | - uses: actions/checkout@v2 42 | if: success() 43 | 44 | - name: Use Node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v1 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | registry-url: 'https://registry.npmjs.org' 49 | 50 | # 安装依赖并构建 51 | - name: Install dependencies & Pack 52 | run: | 53 | node ./scripts/apply-product.js 54 | echo "package.json:" && cat package.json 55 | echo "./build/package.json" && cat ./build/package.json 56 | yarn 57 | yarn download-extension 58 | yarn run pack 59 | env: 60 | SUMI_VERSION: ${{ github.event.inputs.sumiVersion }} 61 | PRODUCT_VERSION: ${{ github.event.inputs.productVersion }} 62 | TARGET_ARCHES: ${{ github.event.inputs.targetArches }} 63 | 64 | - uses: actions/upload-artifact@v3 65 | with: 66 | name: desktop-artifact arm64 67 | path: | 68 | ./out/*-arm64.dmg 69 | 70 | - uses: actions/upload-artifact@v3 71 | with: 72 | name: desktop-artifact x64 73 | path: | 74 | ./out/*-x64.dmg 75 | -------------------------------------------------------------------------------- /scripts/apply-product.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs'); 2 | const path = require('path'); 3 | 4 | function saveWithPrettier(jsonPath, jsonContent) { 5 | try { 6 | const prettier = require('prettier'); 7 | const fileInfo = prettier.getFileInfo.sync(jsonPath, { 8 | resolveConfig: true, 9 | }); 10 | prettier.resolveConfigFile().then((v) => { 11 | prettier.resolveConfig(v).then((options) => { 12 | const content = prettier.format(JSON.stringify(jsonContent), { 13 | parser: fileInfo.inferredParser, 14 | ...options, 15 | }); 16 | writeFileSync(jsonPath, content); 17 | }); 18 | }); 19 | } catch (error) { 20 | console.log('prettier is not installed'); 21 | writeFileSync(jsonPath, JSON.stringify(jsonContent, null, 2)); 22 | } 23 | } 24 | 25 | function saveProductJson() { 26 | const productJson = require('../product.json'); 27 | if (process.env.SUMI_VERSION) { 28 | productJson['sumiVersion'] = String(process.env.SUMI_VERSION).trim(); 29 | } 30 | if (process.env.PRODUCT_VERSION) { 31 | let _version = String(process.env.PRODUCT_VERSION).trim(); 32 | if (_version.startsWith('v')) { 33 | // transform tag version eg. v1.3.6 to 1.3.6 34 | _version = _version.substring(1); 35 | } 36 | productJson['version'] = _version; 37 | } 38 | const jsonPath = path.join(__dirname, '../product.json'); 39 | saveWithPrettier(jsonPath, productJson); 40 | } 41 | 42 | function applySumiVersion() { 43 | const { sumiVersion } = require('../product.json'); 44 | // cancel if sumiVersion not specified 45 | if (!sumiVersion) { 46 | return; 47 | } 48 | 49 | const package = require('../package.json'); 50 | const devDependencies = package['devDependencies']; 51 | const jsonPath = path.join(__dirname, '../package.json'); 52 | 53 | for (const [k] of Object.entries(devDependencies)) { 54 | if (k === '@opensumi/di') { 55 | continue; 56 | } 57 | 58 | if (!k.startsWith('@opensumi/')) { 59 | continue; 60 | } 61 | devDependencies[k] = sumiVersion; 62 | } 63 | 64 | saveWithPrettier(jsonPath, package); 65 | } 66 | 67 | function applyVersion() { 68 | const { version: productVersion } = require('../product.json'); 69 | 70 | const buildPackage = require('../build/package.json'); 71 | buildPackage['version'] = productVersion; 72 | const jsonPath = path.join(__dirname, '../build/package.json'); 73 | saveWithPrettier(jsonPath, buildPackage); 74 | } 75 | 76 | saveProductJson(); 77 | 78 | applySumiVersion(); 79 | applyVersion(); 80 | -------------------------------------------------------------------------------- /src/browser/commands.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommandContribution, 3 | Domain, 4 | CommandRegistry, 5 | ILogger, 6 | createElectronMainApi, 7 | memoize, 8 | } from '@opensumi/ide-core-browser'; 9 | import { Autowired } from '@opensumi/di'; 10 | import { ExtensionService } from '@opensumi/ide-extension/lib/common'; 11 | import { Commands, Constants, ExtensionCommands } from '../common/constants'; 12 | 13 | @Domain(CommandContribution) 14 | export class MainCommandContribution implements CommandContribution { 15 | @Autowired(ExtensionService) 16 | extensionService: ExtensionService; 17 | 18 | @Autowired(ILogger) 19 | logger: ILogger; 20 | 21 | @Autowired(Constants.ELECTRON_NODE_SERVICE_PATH) 22 | nodeService: any; 23 | 24 | _chars = 0; 25 | 26 | @memoize 27 | getMainApi(): any { 28 | return createElectronMainApi(Constants.ELECTRON_MAIN_API_NAME); 29 | } 30 | 31 | tryToJSON(obj: any) { 32 | try { 33 | return JSON.stringify(obj).substr(0, 5000); 34 | } catch (e) { 35 | return 'unable to JSON.stringify'; 36 | } 37 | } 38 | 39 | registerCommands(commands: CommandRegistry): void { 40 | commands.beforeExecuteCommand((command: string, args: any[]) => { 41 | this.logger.log('execute_command', command, ...args.map((a) => this.tryToJSON(a))); 42 | return args; 43 | }); 44 | 45 | commands.registerCommand( 46 | { 47 | id: Commands.OPEN_DEVTOOLS_MAIN, 48 | label: '调试主进程', 49 | }, 50 | { 51 | execute: () => { 52 | this.getMainApi().debugMain(); 53 | }, 54 | }, 55 | ); 56 | 57 | commands.registerCommand( 58 | { 59 | id: Commands.OPEN_DEVTOOLS_NODE, 60 | label: '调试Node进程', 61 | }, 62 | { 63 | execute: () => { 64 | this.nodeService.debugNode(); 65 | }, 66 | }, 67 | ); 68 | 69 | commands.registerCommand( 70 | { 71 | id: Commands.OPEN_DEVTOOLS_EXTENSION, 72 | label: '调试插件进程', 73 | }, 74 | { 75 | execute: () => { 76 | (this.extensionService as any).extensionCommandManager.executeExtensionCommand( 77 | 'node', 78 | ExtensionCommands.OPEN_DEVTOOLS, 79 | [], 80 | ); 81 | }, 82 | }, 83 | ); 84 | commands.registerCommand( 85 | { 86 | id: 'sumi.commandLineTool.install', 87 | label: 'install command line sumi', 88 | }, 89 | { 90 | execute: async () => { 91 | this.nodeService.installShellCommand(); 92 | }, 93 | }, 94 | ); 95 | 96 | commands.registerCommand( 97 | { 98 | id: 'sumi.commandLineTool.uninstall', 99 | label: 'uninstall command line sumi', 100 | }, 101 | { 102 | execute: async () => { 103 | this.nodeService.uninstallShellCommand(); 104 | }, 105 | }, 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '17 6 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /scripts/rebuild-native.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const { join } = require('path'); 3 | const { execSync } = require('child_process'); 4 | const { pathExistsSync, copySync, removeSync } = require('fs-extra'); 5 | const argv = require('yargs').argv; 6 | 7 | const nativeModules = [ 8 | join(__dirname, '../node_modules/node-pty'), 9 | join(__dirname, '../node_modules/@parcel/watcher'), 10 | join(__dirname, '../node_modules/spdlog'), 11 | join(__dirname, '../node_modules/nsfw'), 12 | join(__dirname, '../node_modules/keytar'), 13 | ]; 14 | 15 | let version; 16 | let commands; 17 | 18 | const target = argv.target || 'node'; 19 | const platform = process.env.npm_config_platform || process.platform; 20 | let arch = process.env.npm_config_arch || process.arch || os.arch(); 21 | 22 | if ( 23 | platform === 'darwin' && 24 | process.platform === 'darwin' && 25 | arch === 'x64' && 26 | process.env.npm_config_arch === undefined 27 | ) { 28 | // When downloading for macOS ON macOS and we think we need x64 we should 29 | // check if we're running under rosetta and download the arm64 version if appropriate 30 | try { 31 | const output = execSync('sysctl -in sysctl.proc_translated'); 32 | if (output.toString().trim() === '1') { 33 | arch = 'arm64'; 34 | } 35 | } catch { 36 | // Ignore failure 37 | } 38 | } 39 | 40 | if (target === 'electron') { 41 | version = argv.electronVersion || require('electron/package.json').version; 42 | 43 | console.log('rebuilding native for electron version ' + version); 44 | 45 | if (platform === 'win32') { 46 | commands = [ 47 | 'node-gyp', 48 | 'rebuild', 49 | '--openssl_fips=X', 50 | `--target=${version}`, 51 | `--arch=${arch}`, 52 | '--dist-url=https://electronjs.org/headers', 53 | ]; 54 | } else { 55 | commands = [ 56 | `npm_config_arch=${arch}`, 57 | `npm_config_target_arch=${arch}`, 58 | 'node-gyp', 59 | 'rebuild', 60 | '--openssl_fips=X', 61 | `--target=${version}`, 62 | `--arch=${arch}`, 63 | '--dist-url=https://electronjs.org/headers', 64 | ]; 65 | } 66 | } else if (target === 'node') { 67 | console.log('rebuilding native for node version ' + process.version); 68 | 69 | version = process.version; 70 | 71 | commands = ['node-gyp', 'rebuild']; 72 | } 73 | 74 | function rebuildModule(modulePath, type, version) { 75 | const info = require(join(modulePath, './package.json')); 76 | console.log('rebuilding ' + info.name); 77 | const cache = getBuildCacheDir(modulePath, type, version, target); 78 | if (pathExistsSync(cache) && !argv['force-rebuild']) { 79 | console.log('cache found for ' + info.name); 80 | copySync(cache, join(modulePath, 'build')); 81 | } else { 82 | const command = commands.join(' '); 83 | console.log(command); 84 | execSync(command, { 85 | cwd: modulePath, 86 | HOME: target === 'electron' ? '~/.electron-gyp' : undefined, 87 | }); 88 | removeSync(cache); 89 | copySync(join(modulePath, 'build'), cache); 90 | } 91 | } 92 | 93 | function getBuildCacheDir(modulePath, type, version, target) { 94 | const info = require(join(modulePath, './package.json')); 95 | return join( 96 | require('os').tmpdir(), 97 | 'ide_build_cache', 98 | target, 99 | info.name + '-' + info.version + '-' + arch, 100 | type + '-' + version, 101 | ); 102 | } 103 | 104 | nativeModules.forEach((path) => { 105 | rebuildModule(path, target, version); 106 | }); 107 | -------------------------------------------------------------------------------- /src/browser/project.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Domain, 3 | CommandContribution, 4 | CommandRegistry, 5 | URI, 6 | electronEnv, 7 | ClientAppContribution, 8 | StorageProvider, 9 | } from '@opensumi/ide-core-browser'; 10 | import { IMenuRegistry, MenuId, NextMenuContribution } from '@opensumi/ide-core-browser/lib/menu/next'; 11 | import { Autowired } from '@opensumi/di'; 12 | import { IWorkspaceService } from '@opensumi/ide-workspace/lib/common'; 13 | import { IWindowService, WORKSPACE_COMMANDS } from '@opensumi/ide-core-browser'; 14 | import { ITerminalController } from '@opensumi/ide-terminal-next'; 15 | import { IMainLayoutService } from '@opensumi/ide-main-layout'; 16 | import { BrowserEditorContribution, WorkbenchEditorService } from '@opensumi/ide-editor/lib/browser'; 17 | import { IThemeService } from '@opensumi/ide-theme'; 18 | 19 | @Domain(NextMenuContribution, CommandContribution, BrowserEditorContribution, ClientAppContribution) 20 | export class ProjectSwitcherContribution 21 | implements NextMenuContribution, CommandContribution, BrowserEditorContribution, ClientAppContribution 22 | { 23 | @Autowired(IWorkspaceService) 24 | workspaceService: IWorkspaceService; 25 | 26 | @Autowired(IWindowService) 27 | windowService: IWindowService; 28 | 29 | @Autowired(ITerminalController) 30 | terminalService: ITerminalController; 31 | 32 | @Autowired(WorkbenchEditorService) 33 | editorService: WorkbenchEditorService; 34 | 35 | @Autowired(IMainLayoutService) 36 | private mainLayoutService: IMainLayoutService; 37 | 38 | @Autowired(IThemeService) 39 | private themeService: IThemeService; 40 | 41 | @Autowired(StorageProvider) 42 | getStorage: StorageProvider; 43 | 44 | async onStart() {} 45 | 46 | registerCommands(registry: CommandRegistry) { 47 | this.workspaceService.getMostRecentlyUsedWorkspaces().then((workspaces) => { 48 | workspaces.forEach((workspace) => { 49 | registry.registerCommand( 50 | { 51 | id: 'open.recent.' + workspace, 52 | }, 53 | { 54 | execute: () => { 55 | this.windowService.openWorkspace(new URI(workspace), { 56 | newWindow: true, 57 | }); 58 | }, 59 | }, 60 | ); 61 | }); 62 | }); 63 | } 64 | 65 | registerMenus(registry: IMenuRegistry) { 66 | registry.registerMenuItem(MenuId.MenubarFileMenu, { 67 | submenu: 'recentProjects', 68 | label: '最近项目', 69 | group: '1_open', 70 | }); 71 | 72 | this.workspaceService.getMostRecentlyUsedWorkspaces().then((workspaces) => { 73 | registry.registerMenuItems( 74 | 'recentProjects', 75 | workspaces.map((workspace) => ({ 76 | command: { 77 | id: 'open.recent.' + workspace, 78 | label: new URI(workspace).codeUri.fsPath, 79 | }, 80 | })), 81 | ); 82 | }); 83 | 84 | registry.registerMenuItems(MenuId.MenubarFileMenu, [ 85 | { 86 | command: WORKSPACE_COMMANDS.ADD_WORKSPACE_FOLDER.id, 87 | label: '添加文件夹至工作区', 88 | group: '2_new', 89 | }, 90 | { 91 | command: { 92 | id: WORKSPACE_COMMANDS.SAVE_WORKSPACE_AS_FILE.id, 93 | label: '保存工作区', 94 | }, 95 | group: '3_save', 96 | }, 97 | ]); 98 | } 99 | 100 | onDidRestoreState() { 101 | if (electronEnv.metadata.launchToOpenFile) { 102 | this.editorService.open(URI.file(electronEnv.metadata.launchToOpenFile)); 103 | } 104 | electronEnv.ipcRenderer.on('openFile', (event, file) => { 105 | this.editorService.open(URI.file(file)); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/modules/demo/browser/editor-empty-component.contribution.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState, useEffect, FC, useMemo } from 'react'; 3 | import { 4 | Domain, 5 | ComponentContribution, 6 | ComponentRegistry, 7 | EDITOR_COMMANDS, 8 | SEARCH_COMMANDS, 9 | QUICK_OPEN_COMMANDS, 10 | } from '@opensumi/ide-core-browser'; 11 | import { Keybinding, KeybindingRegistry } from '@opensumi/ide-core-browser/lib/keybinding/keybinding'; 12 | import { useInjectable } from '@opensumi/ide-core-browser/lib/react-hooks'; 13 | import { KeybindingView } from '@opensumi/ide-quick-open/lib/browser/components/keybinding'; 14 | import { localize } from '@opensumi/ide-core-common'; 15 | import { IKeymapService } from '@opensumi/ide-keymaps/lib/common/keymaps'; 16 | 17 | import styles from './editor-empty-component.module.less'; 18 | 19 | /** 20 | * 单行快捷键信息 21 | * @param param0 22 | * @returns 23 | */ 24 | const ShortcutRow: FC<{ 25 | key: string; 26 | label: string; 27 | keybinding: Keybinding; 28 | }> = ({ key, label, keybinding }) => ( 29 |
30 | {label} 31 | 32 |
33 | ); 34 | 35 | /** 36 | * 编辑器空白页引导信息 37 | */ 38 | export const EditorEmptyComponent = () => { 39 | const [imgLoaded, setImgLoaded] = useState(false); 40 | const [keyMapLoaded, setKeyMapLoaded] = useState(false); 41 | 42 | const keybindingRegistry = useInjectable(KeybindingRegistry); 43 | const keymapService = useInjectable(IKeymapService); 44 | 45 | const getKeybinding = (commandId: string) => { 46 | const bindings = keybindingRegistry.getKeybindingsForCommand(commandId); 47 | if (!bindings.length) { 48 | return; 49 | } 50 | 51 | const keyBindings = bindings.sort((a, b) => (b.priority || 0) - (a.priority || 0)); 52 | // 如果快捷键条目没有 when 条件,优先使用 53 | const primaryKeybinding = bindings.find((binding) => !binding.when); 54 | return primaryKeybinding || keyBindings[0]; 55 | }; 56 | 57 | useEffect(() => { 58 | // 监听快捷键是否有更新 59 | keymapService.whenReady.then(() => { 60 | setKeyMapLoaded(true); 61 | }); 62 | }, []); 63 | 64 | const ShortcutView = useMemo(() => { 65 | if (!imgLoaded || !keyMapLoaded) { 66 | return; 67 | } 68 | 69 | const keyInfos = [ 70 | { 71 | label: localize('custom.quick_open'), 72 | command: EDITOR_COMMANDS.QUICK_OPEN.id, 73 | keybinding: getKeybinding(EDITOR_COMMANDS.QUICK_OPEN.id), 74 | }, 75 | { 76 | label: localize('custom.command_palette'), 77 | command: QUICK_OPEN_COMMANDS.OPEN.id, 78 | keybinding: getKeybinding(QUICK_OPEN_COMMANDS.OPEN.id), 79 | }, 80 | { 81 | label: localize('custom.terminal_panel'), 82 | command: 'workbench.view.terminal', 83 | keybinding: getKeybinding('workbench.view.terminal'), 84 | }, 85 | { 86 | label: localize('custom.search_panel'), 87 | command: SEARCH_COMMANDS.OPEN_SEARCH.id, 88 | keybinding: getKeybinding(SEARCH_COMMANDS.OPEN_SEARCH.id), 89 | }, 90 | ].filter((e) => e.keybinding); 91 | return ( 92 |
93 | {keyInfos.map((keyInfo) => ( 94 | 99 | ))} 100 |
101 | ); 102 | }, [imgLoaded, keyMapLoaded]); 103 | 104 | const logoUri = 'https://img.alicdn.com/imgextra/i2/O1CN01dqjQei1tpbj9z9VPH_!!6000000005951-55-tps-87-78.svg'; 105 | return ( 106 |
107 | setImgLoaded(true)} /> 108 | {ShortcutView} 109 |
110 | ); 111 | }; 112 | 113 | @Domain(ComponentContribution) 114 | export class EditorEmptyComponentContribution implements ComponentContribution { 115 | registerComponent(registry: ComponentRegistry) { 116 | registry.register('editor-empty', { 117 | id: 'empty-component', 118 | component: EditorEmptyComponent, 119 | initialProps: {}, 120 | }); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/launch.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { join } from 'path'; 3 | import { app } from 'electron'; 4 | import { existsSync, statSync, ensureDir } from 'fs-extra'; 5 | import { ElectronMainApp } from '@opensumi/ide-core-electron-main'; 6 | import { isOSX, URI } from '@opensumi/ide-core-common'; 7 | import { MainModule } from './services'; 8 | import { OpenSumiDesktopMainModule } from './module'; 9 | import { WebviewElectronMainModule } from '@opensumi/ide-webview/lib/electron-main'; 10 | import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; 11 | import { Injector } from '@opensumi/di'; 12 | import { IMainStorageService } from 'common/types'; 13 | import { MainStorageService } from './services/storage'; 14 | import { Constants } from 'common/constants'; 15 | 16 | const getResourcesPath = () => { 17 | const appPath = app.getAppPath(); 18 | if (appPath.indexOf('app.asar') > -1) { 19 | return join(appPath, '..'); 20 | } 21 | return appPath; 22 | }; 23 | export interface ThemeData { 24 | menuBarBackground?: string; 25 | sideBarBackground?: string; 26 | editorBackground?: string; 27 | panelBackground?: string; 28 | statusBarBackground?: string; 29 | } 30 | 31 | const getExtensionDir = () => join(getResourcesPath(), 'extensions'); 32 | const getUserExtensionDir = () => join(join(os.homedir(), Constants.DATA_FOLDER), 'extensions'); 33 | 34 | if (isOSX) { 35 | process.env.MAC_RESOURCES_PATH = getResourcesPath(); 36 | console.log('MAC_RESOURCES_PATH', process.env.MAC_RESOURCES_PATH); 37 | } 38 | 39 | const injector = new Injector([ 40 | { 41 | token: IMainStorageService, 42 | useClass: MainStorageService, 43 | }, 44 | ]); 45 | const storage: IMainStorageService = injector.get(IMainStorageService); 46 | const themeData: ThemeData = storage.getItemSync('theme'); 47 | 48 | async function init() { 49 | const electronApp: ElectronMainApp = new ElectronMainApp({ 50 | injector, 51 | browserNodeIntegrated: true, 52 | browserUrl: URI.file(join(__dirname, '../browser/index.html')).toString(), 53 | modules: [MainModule, WebviewElectronMainModule, OpenSumiDesktopMainModule], 54 | nodeEntry: join(__dirname, '../node/index.js'), 55 | extensionEntry: join(__dirname, '../extension/index.js'), 56 | extensionWorkerEntry: join(__dirname, '../extension/index.worker.js'), 57 | webviewPreload: join(__dirname, '../webview/host-preload.js'), 58 | plainWebviewPreload: join(__dirname, '../webview/plain-preload.js'), 59 | browserPreload: join(__dirname, '../browser/preload.js'), 60 | extensionDir: getExtensionDir(), 61 | extensionCandidate: [], 62 | overrideBrowserOptions: { 63 | backgroundColor: themeData?.editorBackground || Constants.DEFAULT_BACKGROUND, 64 | trafficLightPosition: { x: 9, y: 6 }, 65 | }, 66 | overrideWebPreferences: {}, 67 | }); 68 | await Promise.all([ensureDir(getExtensionDir()), ensureDir(getUserExtensionDir())]); 69 | return electronApp; 70 | } 71 | 72 | const initPromise = init(); 73 | 74 | export async function launch(workspace?: string) { 75 | console.log('workspace', workspace); 76 | 77 | const electronApp = await initPromise; 78 | await Promise.all([electronApp.init(), app.whenReady()]); 79 | 80 | if (process.env.OPENSUMI_DEVTOOLS === 'true') { 81 | await installExtension(REACT_DEVELOPER_TOOLS, { 82 | loadExtensionOptions: { allowFileAccess: true }, 83 | forceDownload: true, 84 | }) 85 | .then((name) => console.log(`Added Extension: ${name}`)) 86 | .catch((err) => console.error('An error occurred: ', err)); 87 | } 88 | 89 | const codeWindows = electronApp.getCodeWindows(); 90 | 91 | if (!workspace || !existsSync(workspace)) { 92 | if (codeWindows[1]) { 93 | return codeWindows[1].getBrowserWindow().show(); 94 | } 95 | 96 | electronApp.loadWorkspace(undefined, undefined); 97 | return; 98 | } 99 | 100 | const workspaceStat = statSync(workspace); 101 | if (workspaceStat.isDirectory()) { 102 | const workspaceURI = URI.file(workspace); 103 | 104 | for (const window of codeWindows) { 105 | if (window.workspace?.isEqual(workspaceURI)) { 106 | return window.getBrowserWindow().show(); 107 | } 108 | } 109 | 110 | electronApp.loadWorkspace(workspaceURI.toString(), undefined); 111 | return; 112 | } 113 | 114 | if (codeWindows.length) { 115 | codeWindows[0].getBrowserWindow().focus(); 116 | codeWindows[0].getBrowserWindow().webContents.send('openFile', workspace); 117 | return; 118 | } 119 | 120 | electronApp.loadWorkspace(undefined, { launchToOpenFile: workspace }); 121 | } 122 | -------------------------------------------------------------------------------- /src/browser/index.ts: -------------------------------------------------------------------------------- 1 | const win = window as any; 2 | win.Buffer = win.BufferBridge; 3 | if (!(window as any).process) { 4 | (window as any).process = { 5 | browser: true, 6 | env: (window as any).env, 7 | listener: () => [], 8 | }; 9 | } 10 | 11 | import '@opensumi/ide-i18n'; 12 | import { ElectronBasicModule } from '@opensumi/ide-electron-basic/lib/browser'; 13 | import { renderApp } from './app'; 14 | 15 | import { MainLayoutModule } from '@opensumi/ide-main-layout/lib/browser'; 16 | import { MenuBarModule } from '@opensumi/ide-menu-bar/lib/browser'; 17 | import { MonacoModule } from '@opensumi/ide-monaco/lib/browser'; 18 | import { WorkspaceModule } from '@opensumi/ide-workspace/lib/browser'; 19 | import { StatusBarModule } from '@opensumi/ide-status-bar/lib/browser'; 20 | import { EditorModule } from '@opensumi/ide-editor/lib/browser'; 21 | import { ExplorerModule } from '@opensumi/ide-explorer/lib/browser'; 22 | import { FileTreeNextModule } from '@opensumi/ide-file-tree-next/lib/browser'; 23 | import { FileServiceClientModule } from '@opensumi/ide-file-service/lib/browser'; 24 | import { SearchModule } from '@opensumi/ide-search/lib/browser'; 25 | import { FileSchemeModule } from '@opensumi/ide-file-scheme/lib/browser'; 26 | import { OutputModule } from '@opensumi/ide-output/lib/browser'; 27 | import { QuickOpenModule } from '@opensumi/ide-quick-open/lib/browser'; 28 | import { ClientCommonModule, BrowserModule, ConstructorOf } from '@opensumi/ide-core-browser'; 29 | import { ThemeModule } from '@opensumi/ide-theme/lib/browser'; 30 | 31 | import { OpenedEditorModule } from '@opensumi/ide-opened-editor/lib/browser'; 32 | import { OutlineModule } from '@opensumi/ide-outline/lib/browser'; 33 | import { PreferencesModule } from '@opensumi/ide-preferences/lib/browser'; 34 | import { ToolbarModule } from '@opensumi/ide-toolbar/lib/browser'; 35 | import { OverlayModule } from '@opensumi/ide-overlay/lib/browser'; 36 | import { ExtensionStorageModule } from '@opensumi/ide-extension-storage/lib/browser'; 37 | import { StorageModule } from '@opensumi/ide-storage/lib/browser'; 38 | import { SCMModule } from '@opensumi/ide-scm/lib/browser'; 39 | 40 | import { MarkersModule } from '@opensumi/ide-markers/lib/browser'; 41 | import { WebviewModule } from '@opensumi/ide-webview'; 42 | import { MarkdownModule } from '@opensumi/ide-markdown'; 43 | 44 | import { LogModule } from '@opensumi/ide-logs/lib/browser'; 45 | import { WorkspaceEditModule } from '@opensumi/ide-workspace-edit/lib/browser'; 46 | import { ExtensionModule } from '@opensumi/ide-extension/lib/browser'; 47 | import { DecorationModule } from '@opensumi/ide-decoration/lib/browser'; 48 | import { DebugModule } from '@opensumi/ide-debug/lib/browser'; 49 | import { VariableModule } from '@opensumi/ide-variable/lib/browser'; 50 | import { KeymapsModule } from '@opensumi/ide-keymaps/lib/browser'; 51 | import { MonacoEnhanceModule } from '@opensumi/ide-monaco-enhance/lib/browser/module'; 52 | 53 | import { TerminalNextModule } from '@opensumi/ide-terminal-next/lib/browser'; 54 | import { CommentsModule } from '@opensumi/ide-comments/lib/browser'; 55 | 56 | import { ClientAddonModule } from '@opensumi/ide-addons/lib/browser'; 57 | import { TaskModule } from '@opensumi/ide-task/lib/browser'; 58 | 59 | import { DemoModule } from 'modules/demo'; 60 | import { LocalBasicModule } from 'modules/basic/browser'; 61 | 62 | import { customLayoutConfig } from './layout'; 63 | import { MiniDesktopModule } from './module'; 64 | 65 | import { ExtensionManagerModule } from '../extensionManager/browser'; 66 | 67 | export const CommonBrowserModules: ConstructorOf[] = [ 68 | MainLayoutModule, 69 | OverlayModule, 70 | LogModule, 71 | ClientCommonModule, 72 | MenuBarModule, 73 | MonacoModule, 74 | StatusBarModule, 75 | EditorModule, 76 | ExplorerModule, 77 | FileTreeNextModule, 78 | FileServiceClientModule, 79 | SearchModule, 80 | FileSchemeModule, 81 | OutputModule, 82 | QuickOpenModule, 83 | MarkersModule, 84 | ThemeModule, 85 | WorkspaceModule, 86 | ExtensionStorageModule, 87 | StorageModule, 88 | OpenedEditorModule, 89 | OutlineModule, 90 | PreferencesModule, 91 | ToolbarModule, 92 | WebviewModule, 93 | MarkdownModule, 94 | WorkspaceEditModule, 95 | SCMModule, 96 | DecorationModule, 97 | DebugModule, 98 | VariableModule, 99 | KeymapsModule, 100 | TerminalNextModule, 101 | ExtensionModule, 102 | ExtensionManagerModule, 103 | MonacoEnhanceModule, 104 | ClientAddonModule, 105 | CommentsModule, 106 | TaskModule, 107 | MiniDesktopModule, 108 | LocalBasicModule, 109 | ]; 110 | 111 | renderApp({ 112 | modules: [...CommonBrowserModules, ElectronBasicModule, DemoModule], 113 | layoutConfig: customLayoutConfig, 114 | }); 115 | -------------------------------------------------------------------------------- /src/main/services/storage.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { Injector, Injectable, Autowired, INJECTOR_TOKEN } from '@opensumi/di'; 3 | import { URI } from '@opensumi/ide-utils/lib/uri'; 4 | import { Emitter, Event } from '@opensumi/ide-utils/lib/event'; 5 | import fse, { ensureDir, ensureDirSync } from 'fs-extra'; 6 | import { IMainStorageService } from 'common/types'; 7 | import { Constants } from 'common/constants'; 8 | import { Domain } from '@opensumi/ide-core-common'; 9 | import { 10 | ElectronMainApiRegistry, 11 | ElectronMainContribution, 12 | } from '@opensumi/ide-core-electron-main/lib/bootstrap/types'; 13 | 14 | export interface StorageChange { 15 | path: string; 16 | data: string; 17 | } 18 | 19 | const STORAGE_DIR_NAME = ''; 20 | 21 | @Injectable() 22 | export class MainStorageService implements IMainStorageService { 23 | public storageDirUri: URI | undefined; 24 | 25 | public homeDir = os.homedir(); 26 | 27 | public _cache: any = {}; 28 | 29 | public onDidChangeEmitter = new Emitter(); 30 | 31 | readonly onDidChange: Event = this.onDidChangeEmitter.event; 32 | 33 | constructor() { 34 | this.storageDirUri = URI.file(this.homeDir).resolve(Constants.DATA_FOLDER).resolve(STORAGE_DIR_NAME); 35 | } 36 | 37 | setRootStoragePath(storagePath: string) { 38 | if (!storagePath) { 39 | throw new Error('Set Storage path fail, storagePath is incorrect.'); 40 | } 41 | this.storageDirUri = URI.file(storagePath); 42 | } 43 | 44 | async getStoragePath(storageName: string): Promise { 45 | if (!this.storageDirUri) { 46 | throw new Error('No storageDirUri'); 47 | } 48 | 49 | await ensureDir(this.storageDirUri.codeUri.fsPath); 50 | 51 | return this.storageDirUri.resolve(`${storageName}.json`).codeUri.fsPath; 52 | } 53 | 54 | getStoragePathSync(storageName: string): string { 55 | if (!this.storageDirUri) { 56 | throw new Error('No storageDirUri'); 57 | } 58 | 59 | ensureDirSync(this.storageDirUri.codeUri.fsPath); 60 | 61 | return this.storageDirUri.resolve(`${storageName}.json`).codeUri.fsPath; 62 | } 63 | 64 | async getItem(storageName: string): Promise { 65 | if (this._cache[storageName]) { 66 | return this._cache[storageName]; 67 | } 68 | 69 | let data = {}; 70 | const storagePath = await this.getStoragePath(storageName); 71 | try { 72 | await fse.access(storagePath); 73 | } catch (error) { 74 | console.error(`Storage [${storageName}] is invalid.`); 75 | return data; 76 | } 77 | 78 | const content = await fse.readFile(storagePath); 79 | try { 80 | data = JSON.parse(content.toString()); 81 | } catch (error) { 82 | console.error(`Parse item ${storagePath}: ${content.toString()} fail:`, error); 83 | return data; 84 | } 85 | 86 | this._cache[storageName] = data; 87 | return data; 88 | } 89 | 90 | getItemSync(storageName: string): any { 91 | if (this._cache[storageName]) { 92 | return this._cache[storageName]; 93 | } 94 | 95 | let data = {}; 96 | const storagePath = this.getStoragePathSync(storageName); 97 | try { 98 | fse.accessSync(storagePath); 99 | } catch (error) { 100 | console.error(`Storage [${storageName}] is invalid.`); 101 | return data; 102 | } 103 | 104 | const content = fse.readFileSync(storagePath); 105 | try { 106 | data = JSON.parse(content.toString()); 107 | } catch (error) { 108 | console.error(`Parse item ${storagePath}: ${content.toString()} fail:`, error); 109 | return data; 110 | } 111 | 112 | this._cache[storageName] = data; 113 | return data; 114 | } 115 | 116 | async setItem(storageName: string, value: any) { 117 | this._cache[storageName] = value; 118 | let storagePath: string; 119 | try { 120 | storagePath = await this.getStoragePath(storageName); 121 | } catch (error) { 122 | console.error(`Storage [${storageName}] is invalid. ${error.message}`); 123 | return; 124 | } 125 | 126 | if (!value) { 127 | console.error('Trying to setItem null, Not allowed.'); 128 | return; 129 | } 130 | 131 | await fse.writeFile(storagePath, JSON.stringify(value)).catch((error) => { 132 | console.error(`${storagePath} write data fail: ${error.stack}`); 133 | }); 134 | 135 | const change: StorageChange = { 136 | path: URI.parse(storagePath).toString(), 137 | data: value, 138 | }; 139 | this.onDidChangeEmitter.fire(change); 140 | } 141 | } 142 | 143 | @Domain(ElectronMainContribution) 144 | export class MainStorageContribution implements ElectronMainContribution { 145 | @Autowired(INJECTOR_TOKEN) 146 | injector: Injector; 147 | 148 | registerMainApi(registry: ElectronMainApiRegistry) { 149 | registry.registerMainApi(IMainStorageService, this.injector.get(IMainStorageService)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | parser: '@typescript-eslint/parser', 8 | plugins: ['@typescript-eslint'], 9 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'prettier'], 10 | rules: { 11 | '@typescript-eslint/adjacent-overload-signatures': 'error', 12 | '@typescript-eslint/array-type': 'off', 13 | '@typescript-eslint/consistent-type-assertions': 'error', 14 | '@typescript-eslint/consistent-type-definitions': 'error', 15 | '@typescript-eslint/dot-notation': 'off', 16 | '@typescript-eslint/explicit-member-accessibility': [ 17 | 'off', 18 | { 19 | accessibility: 'explicit', 20 | }, 21 | ], 22 | '@typescript-eslint/member-delimiter-style': [ 23 | 'error', 24 | { 25 | multiline: { 26 | delimiter: 'semi', 27 | requireLast: true, 28 | }, 29 | singleline: { 30 | delimiter: 'semi', 31 | requireLast: false, 32 | }, 33 | }, 34 | ], 35 | '@typescript-eslint/member-ordering': 'off', 36 | '@typescript-eslint/naming-convention': 'off', 37 | '@typescript-eslint/no-empty-function': 'off', 38 | '@typescript-eslint/no-empty-interface': 'error', 39 | '@typescript-eslint/no-explicit-any': 'off', 40 | '@typescript-eslint/no-misused-new': 'error', 41 | '@typescript-eslint/no-namespace': 'off', 42 | '@typescript-eslint/no-parameter-properties': 'off', 43 | '@typescript-eslint/no-shadow': [ 44 | 'off', 45 | { 46 | hoist: 'all', 47 | }, 48 | ], 49 | '@typescript-eslint/no-unused-expressions': 'off', 50 | '@typescript-eslint/no-use-before-define': 'off', 51 | '@typescript-eslint/no-var-requires': 'off', 52 | '@typescript-eslint/prefer-for-of': 'error', 53 | '@typescript-eslint/prefer-function-type': 'error', 54 | '@typescript-eslint/prefer-namespace-keyword': 'error', 55 | '@typescript-eslint/quotes': [ 56 | 'error', 57 | 'single', 58 | { 59 | avoidEscape: true, 60 | }, 61 | ], 62 | '@typescript-eslint/semi': ['error', 'always'], 63 | '@typescript-eslint/triple-slash-reference': [ 64 | 'error', 65 | { 66 | path: 'always', 67 | types: 'prefer-import', 68 | lib: 'always', 69 | }, 70 | ], 71 | '@typescript-eslint/type-annotation-spacing': 'error', 72 | '@typescript-eslint/unified-signatures': 'error', 73 | 'arrow-body-style': 'error', 74 | 'arrow-parens': ['error', 'always'], 75 | 'comma-dangle': ['error', 'always-multiline'], 76 | complexity: 'off', 77 | 'constructor-super': 'error', 78 | curly: 'error', 79 | 'eol-last': 'error', 80 | eqeqeq: ['error', 'smart'], 81 | 'guard-for-in': 'error', 82 | 'id-match': 'error', 83 | 'max-classes-per-file': 'off', 84 | 'max-len': 'off', 85 | 'new-parens': 'error', 86 | 'no-bitwise': 'off', 87 | 'no-caller': 'error', 88 | 'no-cond-assign': 'off', 89 | 'no-console': 'warn', 90 | 'no-debugger': 'error', 91 | 'no-constant-condition': ['error', { checkLoops: false }], 92 | // We strongly recommend that you do not use the no-undef lint rule on TypeScript projects. 93 | // The checks it provides are already provided by TypeScript without the need for configuration 94 | // TypeScript just does this significantly better. 95 | 'no-undef': 'off', 96 | 'no-empty': 'off', 97 | 'no-eval': 'off', 98 | 'no-fallthrough': 'off', 99 | 'no-invalid-this': 'off', 100 | 'no-multiple-empty-lines': 'error', 101 | 'no-new-wrappers': 'error', 102 | 'no-throw-literal': 'error', 103 | 'no-trailing-spaces': 'error', 104 | 'no-undef-init': 'error', 105 | 'no-unsafe-finally': 'error', 106 | 'no-unused-labels': 'error', 107 | 'object-shorthand': 'error', 108 | 'one-var': ['error', 'never'], 109 | 'prefer-arrow/prefer-arrow-functions': 'off', 110 | 'quote-props': 'off', 111 | radix: 'error', 112 | 'spaced-comment': [ 113 | 'error', 114 | 'always', 115 | { 116 | markers: ['/'], 117 | }, 118 | ], 119 | 'use-isnan': 'error', 120 | 'valid-typeof': 'off', 121 | 'no-irregular-whitespace': ['error', { skipComments: true }], 122 | 'no-inner-declarations': 'off', 123 | 'no-useless-catch': 'warn', 124 | // TODO: should set below to error in future 125 | 'no-useless-escape': 'warn', 126 | 'no-async-promise-executor': 'warn', 127 | 'prefer-const': 'warn', 128 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn', 129 | '@typescript-eslint/ban-ts-comment': 'warn', 130 | '@typescript-eslint/no-this-alias': 'warn', 131 | '@typescript-eslint/ban-types': 'warn', 132 | 'no-prototype-builtins': 'warn', 133 | 'prefer-rest-params': 'warn', 134 | 'no-control-regex': 'warn', 135 | 'react/prop-types': 'warn', 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /src/node/module.ts: -------------------------------------------------------------------------------- 1 | import { NodeModule, INodeLogger } from '@opensumi/ide-core-node'; 2 | import { openNodeDevtool } from '../common/node/utils'; 3 | import { Autowired, Injectable } from '@opensumi/di'; 4 | import { Constants } from '../common/constants'; 5 | 6 | import * as fs from 'fs'; 7 | import { exists, symlink, unlink } from 'fs'; 8 | 9 | import { promisify } from 'util'; 10 | import { exec } from 'child_process'; 11 | import { lstat, realpath, stat } from 'fs-extra'; 12 | 13 | const existsAsync = promisify(exists); 14 | const unlinkAsync = promisify(unlink); 15 | const symlinkAsync = promisify(symlink); 16 | /** 17 | * Resolves the `fs.Stats` of the provided path. If the path is a 18 | * symbolic link, the `fs.Stats` will be from the target it points 19 | * to. If the target does not exist, `dangling: true` will be returned 20 | * as `symbolicLink` value. 21 | */ 22 | export async function symStat(path: string): Promise { 23 | // First stat the link 24 | let lstats: fs.Stats | undefined; 25 | try { 26 | lstats = await lstat(path); 27 | 28 | // Return early if the stat is not a symbolic link at all 29 | if (!lstats.isSymbolicLink()) { 30 | return { stat: lstats }; 31 | } 32 | } catch (error) { 33 | /* ignore - use stat() instead */ 34 | } 35 | 36 | // If the stat is a symbolic link or failed to stat, use fs.stat() 37 | // which for symbolic links will stat the target they point to 38 | try { 39 | const stats = await stat(path); 40 | 41 | return { stat: stats, symbolicLink: lstats && lstats.isSymbolicLink() ? { dangling: false } : undefined }; 42 | } catch (error) { 43 | // If the link points to a nonexistent file we still want 44 | // to return it as result while setting dangling: true flag 45 | if (error.code === 'ENOENT' && lstats) { 46 | return { stat: lstats, symbolicLink: { dangling: true } }; 47 | } 48 | 49 | throw error; 50 | } 51 | } 52 | 53 | @Injectable() 54 | export class OpenSumiNodeService { 55 | @Autowired(INodeLogger) 56 | logger: INodeLogger; 57 | 58 | debugNode() { 59 | openNodeDevtool(); 60 | } 61 | 62 | async installShellCommand(): Promise { 63 | try { 64 | await this._installShellCommand(); 65 | } catch (error) { 66 | this.logger.error('_installShellCommand', error); 67 | } 68 | } 69 | 70 | async _installShellCommand(): Promise { 71 | const { source, target } = await this.getShellCommandLink(); 72 | 73 | // Only install unless already existing 74 | try { 75 | const { symbolicLink } = await symStat(source); 76 | if (symbolicLink && !symbolicLink.dangling) { 77 | const linkTargetRealPath = await realpath(source); 78 | if (target === linkTargetRealPath) { 79 | return; 80 | } 81 | } 82 | this.logger.error('source', source); 83 | 84 | // Different source, delete it first 85 | await unlinkAsync(source); 86 | } catch (error) { 87 | this.logger.error('error,', error); 88 | if (error.code !== 'ENOENT') { 89 | throw error; // throw on any error but file not found 90 | } 91 | } 92 | 93 | try { 94 | await symlinkAsync(target, source); 95 | } catch (error) { 96 | if (error.code !== 'EACCES' && error.code !== 'ENOENT') { 97 | throw error; 98 | } 99 | 100 | try { 101 | const command = `osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'${target}\' \'${source}\'\\" with administrator privileges"`; 102 | await promisify(exec)(command); 103 | } catch (error) { 104 | throw new Error(`Unable to install the shell command ${source}`); 105 | } 106 | } 107 | } 108 | 109 | async uninstallShellCommand(): Promise { 110 | const { source } = await this.getShellCommandLink(); 111 | 112 | try { 113 | await unlinkAsync(source); 114 | } catch (error) { 115 | switch (error.code) { 116 | case 'EACCES': { 117 | try { 118 | const command = `osascript -e "do shell script \\"rm \'${source}\'\\" with administrator privileges"`; 119 | await promisify(exec)(command); 120 | } catch (error) { 121 | throw new Error(`Unable to uninstall the shell command ${source}`); 122 | } 123 | break; 124 | } 125 | case 'ENOENT': 126 | break; // ignore file not found 127 | default: 128 | throw error; 129 | } 130 | } 131 | } 132 | 133 | private async getShellCommandLink(): Promise<{ readonly source: string; readonly target: string }> { 134 | const target = `${process.env.MAC_RESOURCES_PATH}/resources/darwin/bin/sumi`; 135 | const source = '/usr/local/bin/sumi'; 136 | 137 | // Ensure source exists 138 | const sourceExists = await existsAsync(target); 139 | if (!sourceExists) { 140 | throw new Error(`Unable to find shell script in ${target}`); 141 | } 142 | 143 | return { source, target }; 144 | } 145 | } 146 | 147 | @Injectable() 148 | export class MiniCodeDesktopNodeModule extends NodeModule { 149 | providers = [ 150 | { 151 | token: Constants.ELECTRON_NODE_SERVICE_NAME, 152 | useClass: OpenSumiNodeService, 153 | }, 154 | ]; 155 | 156 | backServices = [ 157 | { 158 | servicePath: Constants.ELECTRON_NODE_SERVICE_PATH, 159 | token: Constants.ELECTRON_NODE_SERVICE_NAME, 160 | }, 161 | ]; 162 | } 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ide-electron", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "OpenSumi IDE Electron 示例项目", 6 | "main": "./app/main/index.js", 7 | "scripts": { 8 | "start": "electron --inspect=9229 .", 9 | "start:no-dev": "electron .", 10 | "link-local": "node ./scripts/link-local.js", 11 | "build:browser": "webpack --config ./build/webpack.browser.config.js ", 12 | "build:node": "webpack --config ./build/webpack.node.config.js ", 13 | "build:extension": "webpack --config ./build/webpack.extension-host.config.js ", 14 | "build:main": "webpack --config ./build/webpack.main.config.js ", 15 | "build:webview": "webpack --config ./build/webpack.webview.config.js ", 16 | "build-prod:browser": "webpack --config ./build/webpack.browser.config.js --mode=production", 17 | "build-prod:node": "webpack --config ./build/webpack.node.config.js --mode=production", 18 | "build-prod:extension": "webpack --config ./build/webpack.extension-host.config.js --mode=production", 19 | "build-prod:main": "webpack --config ./build/webpack.main.config.js --mode=production", 20 | "build-prod:webview": "webpack --config ./build/webpack.webview.config.js --mode=production", 21 | "watch:browser": "webpack --config ./build/webpack.browser.config.js -w --mode=development", 22 | "watch:node": "webpack --config ./build/webpack.node.config.js -w --mode=development", 23 | "watch:extension": "webpack --config ./build/webpack.extension-host.config.js -w --mode=development", 24 | "watch:main": "webpack --config ./build/webpack.main.config.js -w --mode=development", 25 | "watch:webview": "webpack --config ./build/webpack.webview.config.js -w --mode=development", 26 | "watch": "run-p \"watch:*\"", 27 | "build": "rimraf -rf ./app && rimraf -rf ./out && run-p build:browser build:node build:extension build:main build:webview", 28 | "build-prod": "rimraf -rf ./app && rimraf -rf ./out && run-p build-prod:*", 29 | "pack:x64": "yarn build && cross-env TARGET_ARCHES=x64 node build/pack.js", 30 | "pack:arm64": "yarn build && cross-env TARGET_ARCHES=arm64 node build/pack.js", 31 | "pack:all": "yarn pack:arm64 && yarn pack:x64", 32 | "pack": "yarn build-prod && node build/pack.js", 33 | "rebuild-native": "node ./scripts/rebuild-native.js --target=electron", 34 | "download-extension": "cross-env DEBUG=InstallExtension node scripts/download-extensions.js", 35 | "prepare": "husky install" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git@github.com:opensumi/ide-electron.git" 40 | }, 41 | "license": "MIT", 42 | "lint-staged": { 43 | "*.{js,jsx,ts,tsx,md,html,css,less,json}": "prettier --write", 44 | "*.{js,jsx,ts,tsx}": "eslint --fix --quiet" 45 | }, 46 | "devDependencies": { 47 | "@opensumi/ide-addons": "3.2.1", 48 | "@opensumi/ide-comments": "3.2.1", 49 | "@opensumi/ide-core-browser": "3.2.1", 50 | "@opensumi/ide-core-common": "3.2.1", 51 | "@opensumi/ide-core-electron-main": "3.2.1", 52 | "@opensumi/ide-core-node": "3.2.1", 53 | "@opensumi/ide-debug": "3.2.1", 54 | "@opensumi/ide-decoration": "3.2.1", 55 | "@opensumi/ide-editor": "3.2.1", 56 | "@opensumi/ide-electron-basic": "3.2.1", 57 | "@opensumi/ide-explorer": "3.2.1", 58 | "@opensumi/ide-extension": "3.2.1", 59 | "@opensumi/ide-extension-manager": "3.2.1", 60 | "@opensumi/ide-extension-storage": "3.2.1", 61 | "@opensumi/ide-file-scheme": "3.2.1", 62 | "@opensumi/ide-file-search": "3.2.1", 63 | "@opensumi/ide-file-service": "3.2.1", 64 | "@opensumi/ide-file-tree-next": "3.2.1", 65 | "@opensumi/ide-i18n": "3.2.1", 66 | "@opensumi/ide-keymaps": "3.2.1", 67 | "@opensumi/ide-logs": "3.2.1", 68 | "@opensumi/ide-main-layout": "3.2.1", 69 | "@opensumi/ide-markdown": "3.2.1", 70 | "@opensumi/ide-markers": "3.2.1", 71 | "@opensumi/ide-menu-bar": "3.2.1", 72 | "@opensumi/ide-monaco": "3.2.1", 73 | "@opensumi/ide-monaco-enhance": "3.2.1", 74 | "@opensumi/ide-opened-editor": "3.2.1", 75 | "@opensumi/ide-outline": "3.2.1", 76 | "@opensumi/ide-output": "3.2.1", 77 | "@opensumi/ide-overlay": "3.2.1", 78 | "@opensumi/ide-preferences": "3.2.1", 79 | "@opensumi/ide-process": "3.2.1", 80 | "@opensumi/ide-quick-open": "3.2.1", 81 | "@opensumi/ide-scm": "3.2.1", 82 | "@opensumi/ide-search": "3.2.1", 83 | "@opensumi/ide-status-bar": "3.2.1", 84 | "@opensumi/ide-storage": "3.2.1", 85 | "@opensumi/ide-task": "3.2.1", 86 | "@opensumi/ide-terminal-next": "3.2.1", 87 | "@opensumi/ide-testing": "3.2.1", 88 | "@opensumi/ide-theme": "3.2.1", 89 | "@opensumi/ide-toolbar": "3.2.1", 90 | "@opensumi/ide-userstorage": "2.27.2", 91 | "@opensumi/ide-variable": "3.2.1", 92 | "@opensumi/ide-webview": "3.2.1", 93 | "@opensumi/ide-workspace": "3.2.1", 94 | "@opensumi/ide-workspace-edit": "3.2.1", 95 | "@typescript-eslint/eslint-plugin": "^5.15.0", 96 | "@typescript-eslint/parser": "^5.15.0", 97 | "ajv": "^8.11.0", 98 | "await-event": "^2.1.0", 99 | "buffer": "^6.0.3", 100 | "copy-webpack-plugin": "^11.0.0", 101 | "cross-env": "^7.0.3", 102 | "css-loader": "^6.7.1", 103 | "electron": "22.3.25", 104 | "electron-builder": "^23.0.3", 105 | "electron-devtools-installer": "^3.2.0", 106 | "electron-rebuild": "^3.2.7", 107 | "eslint": "^8.11.0", 108 | "eslint-config-prettier": "^8.5.0", 109 | "eslint-plugin-react": "^7.23.2", 110 | "fs-extra": "^8.1.0", 111 | "html-webpack-plugin": "^5.5.0", 112 | "husky": "^7.0.0", 113 | "less": "^4.1.2", 114 | "less-loader": "^11.0.0", 115 | "lint-staged": "^12.3.5", 116 | "mini-css-extract-plugin": "^2.6.0", 117 | "node-fetch": "^2.6.1", 118 | "node-gyp": "^9.1.0", 119 | "npm-run": "^5.0.1", 120 | "npm-run-all": "^4.1.5", 121 | "null-loader": "^4.0.1", 122 | "prettier": "^2.5.1", 123 | "process": "^0.11.10", 124 | "react": "^18.2.0", 125 | "react-dom": "^18.2.0", 126 | "request": "^2.88.2", 127 | "rimraf": "^3.0.0", 128 | "style-loader": "^0.23.1", 129 | "style-resources-loader": "^1.2.1", 130 | "ts-loader": "^9.3.0", 131 | "ts-node": "8.0.2", 132 | "tsconfig-paths": "^3.8.0", 133 | "tsconfig-paths-webpack-plugin": "^3.2.0", 134 | "typescript": "^4.6.2", 135 | "webpack": "^5.72.1", 136 | "webpack-cli": "^4.9.1" 137 | }, 138 | "peerDependencies": { 139 | "@opensumi/di": "*", 140 | "node-gyp": "*" 141 | }, 142 | "dependencies": { 143 | "@opensumi/ide-ai-native": "3.2.1", 144 | "@opensumi/ide-design": "3.2.1" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /scripts/download-extensions.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const rimraf = require('rimraf'); 3 | const mkdirp = require('mkdirp'); 4 | const fs = require('fs-extra'); 5 | const yauzl = require('yauzl'); 6 | const log = require('debug')('InstallExtension'); 7 | const os = require('os'); 8 | const got = require('got'); 9 | const nodeFetch = require('node-fetch'); 10 | const awaitEvent = require('await-event'); 11 | const { v4 } = require('uuid'); 12 | 13 | // 放置 vscode extension 的目录 14 | const targetDir = path.resolve(__dirname, '../extensions/'); 15 | 16 | const { extensions } = require(path.resolve(__dirname, '../build/extensions.json')); 17 | 18 | // 限制并发数,运行promise 19 | const parallelRunPromise = (lazyPromises, n) => { 20 | const results = []; 21 | let index = 0; 22 | let working = 0; 23 | let complete = 0; 24 | 25 | const addWorking = (res, rej) => { 26 | while (working < n && index < lazyPromises.length) { 27 | const current = lazyPromises[index++]; 28 | working++; 29 | 30 | ((index) => { 31 | current().then((result) => { 32 | working--; 33 | complete++; 34 | results[index] = result; 35 | 36 | if (complete === lazyPromises.length) { 37 | res(results); 38 | return; 39 | } 40 | 41 | // note: 虽然addWorking中有while,这里其实每次只会加一个promise 42 | addWorking(res, rej); 43 | }, rej); 44 | })(index - 1); 45 | } 46 | }; 47 | 48 | return new Promise(addWorking); 49 | }; 50 | 51 | const api = 'https://open-vsx.org/api/'; 52 | 53 | async function downloadExtension(url, namespace, extensionName) { 54 | const tmpPath = path.join(os.tmpdir(), 'extension', v4()); 55 | const tmpZipFile = path.join(tmpPath, path.basename(url)); 56 | await fs.mkdirp(tmpPath); 57 | 58 | const tmpStream = fs.createWriteStream(tmpZipFile); 59 | const data = await got.stream(url, { timeout: 100000 }); 60 | 61 | data.pipe(tmpStream); 62 | await Promise.race([awaitEvent(data, 'end'), awaitEvent(data, 'error')]); 63 | tmpStream.close(); 64 | 65 | const targetDirName = path.basename(`${namespace}.${extensionName}`); 66 | 67 | return { tmpZipFile, targetDirName }; 68 | } 69 | 70 | function openZipStream(zipFile, entry) { 71 | return new Promise((resolve, reject) => { 72 | zipFile.openReadStream(entry, (error, stream) => { 73 | if (error) { 74 | reject(error); 75 | } else { 76 | resolve(stream); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | function modeFromEntry(entry) { 83 | const attr = entry.externalFileAttributes >> 16 || 33188; 84 | 85 | return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */] 86 | .map((mask) => attr & mask) 87 | .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */); 88 | } 89 | 90 | function createZipFile(zipFilePath) { 91 | return new Promise((resolve, reject) => { 92 | yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => { 93 | if (err) { 94 | reject(err); 95 | } 96 | resolve(zipfile); 97 | }); 98 | }); 99 | } 100 | 101 | function unzipFile(dist, targetDirName, tmpZipFile) { 102 | const sourcePathRegex = new RegExp('^extension'); 103 | // eslint-disable-next-line no-async-promise-executor 104 | return new Promise(async (resolve, reject) => { 105 | try { 106 | const extensionDir = path.join(dist, targetDirName); 107 | // 创建插件目录 108 | await fs.mkdirp(extensionDir); 109 | 110 | const zipFile = await createZipFile(tmpZipFile); 111 | zipFile.readEntry(); 112 | zipFile.on('error', (e) => { 113 | reject(e); 114 | }); 115 | 116 | zipFile.on('close', () => { 117 | if (!fs.pathExistsSync(path.join(extensionDir, 'package.json'))) { 118 | reject(`Download Error: ${extensionDir}/package.json`); 119 | return; 120 | } 121 | fs.remove(tmpZipFile).then(() => resolve(extensionDir)); 122 | }); 123 | 124 | zipFile.on('entry', (entry) => { 125 | if (!sourcePathRegex.test(entry.fileName)) { 126 | zipFile.readEntry(); 127 | return; 128 | } 129 | let fileName = entry.fileName.replace(sourcePathRegex, ''); 130 | 131 | if (/\/$/.test(fileName)) { 132 | const targetFileName = path.join(extensionDir, fileName); 133 | fs.mkdirp(targetFileName).then(() => zipFile.readEntry()); 134 | return; 135 | } 136 | 137 | let originalFileName; 138 | // 在Electron中,如果解包的文件中存在.asar文件,会由于Electron本身的bug导致无法对.asar创建writeStream 139 | // 此处先把.asar文件写到另外一个目标文件中,完成后再进行重命名 140 | if (fileName.endsWith('.asar') && this.options.isElectronEnv) { 141 | originalFileName = fileName; 142 | fileName += '_prevent_bug'; 143 | } 144 | const readStream = openZipStream(zipFile, entry); 145 | const mode = modeFromEntry(entry); 146 | readStream.then((stream) => { 147 | const dirname = path.dirname(fileName); 148 | const targetDirName = path.join(extensionDir, dirname); 149 | if (targetDirName.indexOf(extensionDir) !== 0) { 150 | throw new Error(`invalid file path ${targetDirName}`); 151 | } 152 | const targetFileName = path.join(extensionDir, fileName); 153 | 154 | fs.mkdirp(targetDirName).then(() => { 155 | const writerStream = fs.createWriteStream(targetFileName, { mode }); 156 | writerStream.on('close', () => { 157 | if (originalFileName) { 158 | // rename .asar, if filename has been modified 159 | fs.renameSync(targetFileName, path.join(extensionDir, originalFileName)); 160 | } 161 | zipFile.readEntry(); 162 | }); 163 | stream.on('error', (err) => { 164 | throw err; 165 | }); 166 | stream.pipe(writerStream); 167 | }); 168 | }); 169 | }); 170 | } catch (err) { 171 | reject(err); 172 | } 173 | }); 174 | } 175 | 176 | const installExtension = async (namespace, name, version) => { 177 | const path = version ? `${namespace}/${name}/${version}` : `${namespace}/${name}`; 178 | const res = await nodeFetch(`${api}${path}`, { 179 | timeout: 100000, 180 | }); 181 | const data = await res.json(); 182 | if (data.files && data.files.download) { 183 | const { targetDirName, tmpZipFile } = await downloadExtension(data.files.download, namespace, name); 184 | // 解压插件 185 | await unzipFile(targetDir, targetDirName, tmpZipFile); 186 | rimraf.sync(tmpZipFile); 187 | } 188 | }; 189 | 190 | const downloadVscodeExtensions = async () => { 191 | log('清空 vscode extension 目录:%s', targetDir); 192 | rimraf.sync(targetDir); 193 | mkdirp.sync(targetDir); 194 | 195 | const promises = []; 196 | const publishers = Object.keys(extensions); 197 | for (const publisher of publishers) { 198 | const items = extensions[publisher]; 199 | 200 | for (const item of items) { 201 | const { name, version } = item; 202 | promises.push(async () => { 203 | log('开始安装:%s', name, version); 204 | try { 205 | await installExtension(publisher, name, version); 206 | } catch (e) { 207 | console.log(`${name} 插件安装失败: ${e.message}`); 208 | } 209 | }); 210 | } 211 | } 212 | 213 | // 限制并发 promise 数 214 | await parallelRunPromise(promises, 3); 215 | log('安装完毕'); 216 | }; 217 | 218 | // 执行并捕捉异常 219 | downloadVscodeExtensions().catch((e) => { 220 | console.trace(e); 221 | rimraf(); 222 | process.exit(128); 223 | }); 224 | --------------------------------------------------------------------------------