OpenSumi官网文档熟悉
# 简介
# Sumi与Theia的比较
# 概览
# 整体架构
在 OpenSumi 内主要有三个核心进程:
- 插件进程 (Extension Process)
- 后端进程 (Node Process)
- 前端进程 (Browser Process)
为了保证插件的问题不会影响 IDE 的性能表现,插件能力上 OpenSumi 采用了跟 VS Code
类似的方案,通过独立的插件进程去启动插件,插件进程再通过后端进程与前端进程进行通信。
OpenSumi 的不同能力实现被拆分到了不同的模块内,这些模块通过 贡献点机制 (Contribution Point) (opens new window)、DI 机制 (Dependence Inject) (opens new window) 互相之间有较弱的依赖关系,对于一些比较核心的基础模块,如主题服务、布局服务等,也会被其他模块直接依赖。
因此,在集成开发过程中需要保证一些模块的引入顺序。
整体启动的生命周期如下图所示:
# 模块
模块是指 OpenSumi (opens new window) 中 package
目录下的一个包,它可以发布到 npm
,并通过集成时安装依赖的方式引用。
通常情况下,模块是一个独立的功能实现,例如 debug
模块基于 DAP (opens new window) 实现了一层通用的调试适配器,包括了调试器前端、会话管理等核心功能。并通过插件 API 的方式将其提供给插件调用。
一个模块的基本结构如下
.
├── __tests__
│ ├── browser
│ └── node
└── src
│ ├── browser
│ ├── common
│ └── node
└── webpack.config.js
└── package.json
└── README.md
2
3
4
5
6
7
8
9
10
11
在 OpenSumi (opens new window) 中,你可以通过
yarn run create [模块名]
的方式自动创建并关联引用关系。
模块即可以包含 Browser
层代码,也可以包含 Node
层代码
browser
层代码一般用于处理视图相关的能力,以 OpenSumi 中的search
模块为例,搜索的界面就是由browser
层进行实现。node
层代码一般用于处理需要使用到Node.js
能力的逻辑,例如搜索面板中的全局搜索能力就需要node
层进行实现。common
层一般用于存放一些公共的变量、工具方法、类型声明等。
# 拓展 Browser 层能力
我们通过以下的文件结构拓展 Browser
层能力,你可以通过在 providers
中声明相关内容来拓展能力,详细案例可见我们的 Todo List 案例 (opens new window)。
// Browser 模块入口
import { Provider, Injectable } from '@opensumi/di';
import { BrowserModule } from '@opensumi/ide-core-browser';
import { HelloWorld } from './hello-world.view';
@Injectable()
export class ModuleDemoModule extends BrowserModule {
providers: Provider[] = [];
}
2
3
4
5
6
7
8
9
# 拓展 Node 层能力
我们通过以下的文件结构拓展 Node
层能力,你可以通过在 providers
中声明相关内容来拓展能力,详细案例可见我们的 Todo List 案例 (opens new window)。
// Node 模块入口
import { Provider, Injectable } from '@opensumi/di';
import { NodeModule } from '@opensumi/ide-core-node';
@Injectable()
export class ModuleDemoModule extends NodeModule {
providers: Provider[] = [];
}
2
3
4
5
6
7
8
# 插件
插件, 也可称为 Extension,是指当前 OpenSumi 框架下支持的,通过在特定位置安装插件文件,从而对 IDE 的界面、功能进行二次插件的一类程序文件,设计上兼容 VSCode
中的 VSCode Extension API (opens new window),即对于使用 OpenSumi 进行开发的 IDE 产品天然兼容 VSCode
的插件体系。
针对 OpenSumi 插件开发相关的研发流程链路支持将会在后续逐步推出,敬请期待。
# 如何发布
由于协议问题,OpenSumi 无法直接使用 VS Code 插件市场源,当前 OpenSumi 默认集成了 Eclipse 公司研发的 Eclipse Open VSX (opens new window) 服务,开发者可以直接使用,也可以基于 Eclipse Open VSX (opens new window) 建设自己的插件市场, 后续,我们也将建设自己的插件市场开发免费的插件托管服务给更多开发者使用。
参考文档:Publishing Extensions (opens new window)
# 快速开始
# 快速开始(Web)
OpenSumi 基于 Node.js 12.x +
版本开发,需要确保你本地已经安装上正确的 Node.js 版本。同时 OpenSumi 依赖一些 Node.js Addon,为了确保这些 Addon 能够被正常编译运行,建议参考 node-gyp (opens new window) 中的安装指南来搭建本地环境。
# 本地启动
注意:由于编译过程中需要下载大量的包,并且部分包需要访问 GitHub 下载源码,请保持 GitHub 的访问畅通。很多 404 Not Found 的问题都是网络访问失败引起的。
依次运行下面的命令:
$ git clone git@github.com:opensumi/ide-startup.git
$ cd ide-startup
$ yarn # 安装依赖
$ yarn start # 并行启动前端和后端
2
3
4
浏览器打开 http://127.0.0.1:8080
进行预览或开发。
# 使用 Docker 镜像
# 拉取镜像
docker pull ghcr.io/opensumi/opensumi-web:latest
# 运行
docker run --rm -d -p 8080:8000/tcp ghcr.io/opensumi/opensumi-web:latest
2
3
4
5
浏览器打开 http://127.0.0.1:8080
进行预览或开发。
# 启动参数
Startup 中集成代码比较简单,大体上是分别实例化了 ClientApp
和 ServerApp
,传入相应的参数并启动。
详细启动参数可查看 自定义配置 (opens new window) 文档。
# 定制 IDE
OpenSumi 支持通过模块的方式对界面主题、内置命令、菜单等基础能力进行定制,更多详细的定制内容可以查看:
# 快速开始(纯前端)
# 概览
OpenSumi 提供了纯前端版本的接入能力,可以让你脱离 node 的环境,在纯浏览器环境下,通过简单的 B/S 架构提供相对完整的 IDE 能力。
在开始运行前,请先保证本地的环境已经安装 Node.js 10.15.x 或以上版本。同时 OpenSumi 依赖一些 Node.js Addon,为了确保这些 Addon 能够被正常编译运行,建议参考 node-gyp (opens new window) 中的安装指南来搭建本地环境。
同时,你也可以直接访问我们的预览页面 (opens new window)体验最新运行效果,支持分支或者 tag 地址 如 https://opensumi.github.io/ide-startup-lite/#https://github.com/opensumi/core/tree/v2.16.0
。
# 快速开始
克隆项目 opensumi/ide-startup-lite
,进入目录执行以下命令启动 IDE:
$ git clone https://github.com/opensumi/ide-startup-lite.git
$ cd ide-startup-lite
$ npm install # 安装依赖
$ npm run compile:ext-worker # 编译 webworker 插件环境
$ npm run start # 启动
2
3
4
5
浏览器打开 http://127.0.0.1:8080
进行预览或开发。
距离一个完整可用的纯前端版 IDE 还需要以下实现:
- 文件服务配置 *(必选)
- 插件配置
- 语言能力配置
- 搜索服务配置
# 文件服务配置
纯前端版本使用 BrowserFsProvider
替换 OpenSumi 内的 DiskFileSystemProvider
, 改动在于由原来的本地文件服务改成 http 接口服务。
文件位置:
web-lite/file-provider/browser-fs-provider.ts
# 文件服务
与容器版、electron 版这种全功能 IDE 不同的是,纯前端版本 IDE 一般都服务于一个垂直、特定的场景,比如代码查看、codereview 等等,对应的底层能力是服务化的。且由于浏览器本身没有文件系统,因此需要一个外部的数据源来提供和维护文件信息。在纯前端版本,我们需要开发者实现以下两个方法来支持基础的代码查看能力:
文件位置:
web-lite/file-provider/http-file-service.ts
readDir(uri: Uri): Promise<Array<{type: ‘tree’ | ‘leaf’, path: string}>>
:返回目录结构信息readFile(uri: Uri, encoding?: string): Promise<string>
:返回文件内容
实现上述两个方法即可支持只读模式下的 IDE 能力。如果需要支持代码编辑能力,还需要实现下面三个方法:
updateFile(uri: Uri, content: string, options: { encoding?: string; newUri?: Uri; }): Promise<void>
createFile(uri: Uri, content: string, options: { encoding?: string; }): Promise<void>
deleteFile(uri: Uri, options: { recursive?: boolean }): Promise<void>
代码修改后,会先调用对应方法同步到集成方的服务端,之后浏览器端也会在内存中缓存一份新的代码,刷新后失效。
# 语法高亮及代码提示
# 语法高亮
出于性能考虑,纯前端版本的静态语法高亮能力默认不通过插件来注册,我们将常见的语法封装到了一个统一的 npm 包内,直接声明想要支持的语法即可:
文件位置:
web-lite/grammar/index.contribution.ts
const languages = [
‘html’,
‘css’,
‘javascript’,
‘less’,
‘markdown’,
‘typescript’,
];
2
3
4
5
6
7
8
注:我们提供了直接 Require 和动态 import 两种方式来引入语法声明文件,前者会使得 bundleSize 变大,后者部署成本会更高,集成时可自行选择
# 单文件语法服务
OpenSumi 基于纯前端插件(worker 版)能力,提供了常见语法的基础提示。由于没有文件服务,worker 版本语法提示插件只支持单文件的代码提示,不支持跨文件分析,对于纯前端的轻量编辑场景而言,基本上是够用的。目前可选的语法提示插件有:
const languageExtensions = [
{ id: 'alex.typescript-language-features-worker', version: '1.0.0-beta.2' },
{ id: 'alex.markdown-language-features-worker', version: '1.0.0-beta.2' },
{ id: 'alex.html-language-features-worker', version: '1.0.0-beta.1' },
{ id: 'alex.json-language-features-worker', version: '1.0.0-beta.1' },
{ id: 'alex.css-language-features-worker', version: '1.0.0-beta.1' }
];
2
3
4
5
6
7
将语法提示插件直接加入插件列表即可。
# Lsif 语法服务
对于代码查看、Code review 这一类纯浏览场景,基于离线索引分析的 LSIF 方案 (opens new window) 可以很好的支持跨文件 Hover 提示,代码跳转的需求,且不需要浏览器端承担任何额外的分析开销。OpenSumi 纯前端版集成了 lsif client,只需要简单的对接即可接入 lsif 服务:
文件位置:
web-lite/language-service/lsif-service/lsif-client.ts
export interface ILsifPayload {
repository: string;
commit: string;
path: string;
character: number;
line: number;
}
export interface ILsifClient {
exists(repo: string, commit: string): Promise<boolean>;
hover(params: ILsifPayload): Promise<vscode.Hover>;
definition(params: ILsifPayload): Promise<vscode.Location[]>;
reference(params: ILsifPayload): Promise<vscode.Location[]>;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 搜索能力
搜索功能属于可选实现,默认不开启搜索功能。实现搜索能力的核心在于实现 file-search 和 search 模块的后端部分。
# 文件搜索
要实现文件搜索功能(通过 cmd+p 触发),需要实现以下方法:
export interface IFileSearchService {
/**
* finds files by a given search pattern.
* @return the matching paths, relative to the given `options.rootUri`.
*/
find(
searchPattern: string,
options: IFileSearchService.Options,
cancellationToken?: CancellationToken
): Promise<string[]>;
}
2
3
4
5
6
7
8
9
10
11
实现后替换默认的 mock-file-seach.ts 即可。
# 文件内容搜索
文件内容搜索功能的实现需要改造 search.service.ts
,暂不提供官方实现。
# 常见集成场景
# 自定义配置
# 注册自定义配置
常用的注册自定义配置主要有如下两种方式:
- 在集成侧通过模块贡献点注册
- 通过插件的
Configurations
贡献点注册
OpenSumi 提供了自定义配置能力,基于 OpenSumi 的 Contribution Point (opens new window) 机制,只需要实现 PreferenceContribution
即可进行配置注册。
另一种注册方式则是通过插件的 configuration 贡献点 (opens new window) 在插件中进行注册。
# 自定义集成参数
在集成 OpenSumi 框架的时候,我们往往需要进行独立的配置,下面列举了一些可在集成阶段通过传入配置项进行配置的参数:
# Browser 配置
定义可见 @opensumi/ide-core-browser
中的 AppConfig
定义。
# Node 配置
定义可见 @opensumi/ide-core-node
中的 AppConfig
定义。
# 自定义菜单
# 注册自定义菜单
注册自定义菜单,常见的有两种模式:
- 注册新的菜单项
- 向已有的菜单项注册子菜单
OpenSumi 提供了自定义菜单能力,基于 OpenSumi 的 Contribution (opens new window) 机制,实现 MenuContribution
,调用 menuRegistry
提供的方法即可。
interface MenuContribution {
registerMenus?(menus: IMenuRegistry): void;
}
interface IMenuRegistry {
// 注册新的菜单项
registerMenubarItem(
menuId: string,
item: PartialBy<IMenubarItem, 'id'>
): IDisposable;
// 向已有的菜单项注册子菜单
registerMenuItem(
menuId: MenuId | string,
item: IMenuItem | ISubmenuItem | IInternalComponentMenuItem
): IDisposable;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 注册新的菜单项
例如我们希望注册一个新的 终端
菜单项,并希望它展示在第一项,调用 registry.registerMenuBarItem
, 同时传入 order: 0
表示其定位在第一项。
import {
MenuContribution,
IMenuRegistry,
MenuId
} from '@opensumi/ide-core-browser/lib/menu/next';
const terminalMenuBarId = 'menubar/terminal';
@Domain(MenuContribution)
class MyMenusContribution implements MenuContribution {
registerMenus(registry: IMenuRegistry) {
registry.registerMenubarItem(terminalMenuBarId, {
label: '终端',
order: 0
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 向已有的菜单项注册子菜单
我们将终端
菜单项注册在了菜单栏的第一项,但它还没有子菜单,点击后没有任何反应,我们需要再为其注册一组子菜单。调用 registry
的 registerMenuItem
可以注册单个菜单项,也可以使用 registerMenuItems
方法来批量注册多个子菜单项。 菜单点击后需要执行某些操作,在这个例子中,我们希望点击后拆分终端,需要为其绑定一个 Command
,Command
也一样可以通过实现 CommandContribution
来自定义 (opens new window),在这里我们使用内置的 terminal.split
命令。
注意,当绑定的 Command 在注册时也指定了
label
属性时,注册菜单项的 label 默认不会生效
registerMenus(registry: IMenuRegistry) {
registry.registerMenubarItem(terminalMenuBarId, { label: '终端', order: 0 });
registry.registerMenuItem(terminalMenuBarId, {
command: 'terminal.split',
group: '1_split',
});
}
2
3
4
5
6
7
8
# 子菜单分组
当注册的菜单较多时,我们可能希望将一些类似操作的子菜单与其他菜单间隔起来,可以使用 group
属性来为子菜单分组。具体来说,就是为这些类似操作
的菜单使用相同的 group
值即可。这里我们使用 registry.registerMenuItems
来注册更多子菜单。
registerMenus(registry: IMenuRegistry) {
registry.registerMenubarItem(terminalMenuBarId, { label: '终端', order: 0 });
registry.registerMenuItems(terminalMenuBarId, [
{
command: 'terminal.split',
group: '1_split',
},
{
command: 'terminal.remove',
group: '2_clear',
},
{
command: 'terminal.clear',
group: '2_clear',
},
{
command: 'terminal.search',
group: '3_search',
},
{
command: 'terminal.search.next',
group: '3_search',
},
]);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 注册二级子菜单
对于同类型的菜单,除了使用 group
来将它们分组之外,还可以将其注册为二级子菜单
,当子菜单较多时,使用二级菜单能有效的改善用户体验。例如我们希望将 搜索
以及 搜索下一个匹配项
都注册为 搜索
的二级菜单。
const searchSubMenuId = 'terminal/search';
registerMenus(registry: IMenuRegistry) {
registry.registerMenubarItem(terminalMenuBarId, { label: '终端', order: 0 });
registry.registerMenuItems(terminalMenuBarId, [
{
command: 'terminal.split',
group: '1_split',
},
{
command: 'terminal.remove',
group: '2_clear',
},
{
command: 'terminal.clear',
group: '2_clear',
},
{
label: '搜索',
group: '3_search',
submenu: searchSubMenuId,
},
]);
registry.registerMenuItems(searchSubMenuId, [
{
command: 'terminal.search',
},
{
command: 'terminal.search.next',
},
]);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 自定义视图
# 概览
OpenSumi 视图基于 插槽机制
设计,整个 Layout 本身是一个大的 React 组件,组件会将视图划分为若干个插槽。如 OpenSumi 默认提供的布局组件就会将视图划分为如下图所示的插槽模板:
插槽用于组件视图的挂载,每个插槽会消费一个如下数据结构的数据:
type ComponentsInfo = Array<{
views: View[];
options?: ViewContainerOptions;
}>;
export interface View {
// 无关选项已被隐藏
component: React.ComponentType<any>;
}
2
3
4
5
6
7
8
插槽渲染器
决定了这个数据的消费方式。默认情况下,视图会从上而下平铺布局。在侧边栏、底部栏位置,插槽渲染器默认为支持折叠展开和切换的 TabBar 组件。除了侧边栏区域会通过手风琴的方式支持多个子视图外,其余位置默认只会消费 views 的第一个视图。
数据的提供方为视图配置 LayoutConfig,其数据结构如下:
export const defaultConfig: LayoutConfig = {
[SlotLocation.top]: {
modules: ['@opensumi/ide-menu-bar']
},
[SlotLocation.left]: {
modules: [
'@opensumi/ide-explorer',
'@opensumi/ide-search',
'@opensumi/ide-scm',
'@opensumi/ide-extension-manager',
'@opensumi/ide-debug'
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
视图的 Token 与真实的 React 组件通过 ComponentContribution 进行注册关联。
import { Search } from './search.view';
@Domain(ComponentContribution)
export class SearchContribution implements ComponentContribution {
registerComponent(registry: ComponentRegistry) {
registry.register('@opensumi/ide-search', [], {
containerId: SEARCH_CONTAINER_ID,
iconClass: getIcon('search'),
title: localize('search.title'),
component: Search,
priority: 9
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
下面以在 MenuBar 右侧增加一个 ToolBar 组件为例,介绍如何定制 Layout。
# 视图注册
首先我们需要将 ToolBar 组件进行注册,关联到一个字符串 Token test-toolbar
上:
export const Toolbar = () => (
<div style={{ lineHeight: '27px' }}>I'm a ToolBar, ToolBar, ToolBar</div>
);
@Domain(ComponentContribution)
export class TestContribution implements ComponentContribution {
registerComponent(registry: ComponentRegistry) {
registry.register(
'test-toolbar',
[
{
id: 'test-toolbar',
component: Toolbar,
name: '测试'
}
],
{
containerId: 'test-toolbar'
}
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 视图消费
对于该需求,支持视图的渲染有两种方案:
- 替换顶部位置的插槽渲染器,支持左右平铺
- 在布局组件上划出一个新的插槽位置,单独支持 ToolBar 注册
# 定制插槽渲染器
通过 SlotRenderer Contribution 替换顶部的 SlotRenderer,将默认的上下平铺模式改成横向的 flex 模式:
export const TopSlotRenderer: (props: {
className: string;
components: ComponentRegistryInfo[];
}) => any = ({ className, components }) => {
const tmp = components.map(item => item.views[0].component!);
return (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{tmp.map((Component, index) => (
<Component key={index} />
))}
</div>
);
};
@Domain(SlotRendererContribution)
export class SampleContribution implements SlotRendererContribution {
registerRenderer(registry: SlotRendererRegistry) {
registry.registerSlotRenderer(SlotLocation.top, TopSlotRenderer);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
之后在视图配置里将 ToolBar 的视图传入顶部位置即可:
const layoutConfig = {
[SlotLocation.top]: {
modules: ['@opensumi/ide-menu-bar', 'test-ToolBar']
}
// rest code
};
renderApp({
layoutConfig
// rest code
});
2
3
4
5
6
7
8
9
10
# 增加插槽位置
增加插槽位置非常简单,只需要将 SlotRenderer 组件放入视图即可,Layout 设计的很灵活,你可以在任意位置插入这个渲染器。在本例中,可以选择在布局组件中增加该位置,或在 MenuBar 视图内增加该位置:
// 在布局模板上增加
export function LayoutComponent() {
return (
<BoxPanel direction="top-to-bottom">
<BoxPanel direction="left-to-right">
<SlotRenderer
color={colors.menuBarBackground}
defaultSize={27}
slot="top"
/>
// 增加一个slot插槽
<SlotRenderer
color={colors.menuBarBackground}
defaultSize={27}
slot="action"
/>
</BoxPanel>
// rest code
</BoxPanel>
);
}
// 在MenuBar视图内增加
export const MenuBarMixToolbarAction: React.FC<MenuBarMixToolbarActionProps> = props => {
return (
<div className={clx(styles.MenuBarWrapper, props.className)}>
<MenuBar />
<SlotRenderer slot="action" flex={1} overflow={'initial'} />
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
增加好插槽位置后,在视图配置里增加对应位置及相应的视图 Token 即可:
const layoutConfig = {
[SlotLocation.action]: {
modules: ['test-toolbar']
}
// rest code
};
2
3
4
5
6
# 扩展阅读
一般情况下,使用上述示例的方式就可以完成常见的布局定制需求支持,但是对于一些需要拖拽改变尺寸功能、视图切换功能的定制场景,直接使用原生 HTML 开始写的话会比较复杂,且交互不一致,OpenSumi 提供了可用于搭建布局的几类基础组件:
- 布局基础组件
- BoxPanel 组件,普通 Flex 布局组件,支持不同方向的 Flex 布局
- SplitPanel 组件,支持鼠标拖拽改变尺寸的 BoxPanel
- 插槽渲染器实现组件
- Accordion 组件,手风琴组件,支持 SplitPanel 的所有能力,同时支持子视图面板的折叠展开控制
- TabBar 组件,多 Tab 管理组件,支持视图的激活、折叠、展开、切换,支持 Tab 拖拽更换位置
- TabPanel 组件,Tab 渲染组件,侧边栏为 Panel Title + Accordion,底部栏为普通 React 视图
具体的组件使用方式可以参考组件的类型声明。
# 自定义命令
# 概览
使用命令的场景主要有下面两种:
# 插件
插件中可以通过下面例子的用法使用 OpenSumi 中的命令:
import { commands, Uri } from 'sumi';
// OpenSumi 针对插件进程的命令调用进行了特殊处理,真实执行时会将 `Uri` 转化为 `URI`
let uri = Uri.file('/some/path/to/folder');
let success = await commands.executeCommand('vscode.openFolder', uri);
2
3
4
5
# 模块
模块中可以通过下面例子的用法使用 OpenSumi 中的命令:
import { Injectable, Autowired } from '@opensumi/common-di';
import { CommandService, URI } from '@opensumi/ide-core-browser';
@Injectable()
class DemoModule {
...
@Autowired(CommandService)
private readonly commandService: CommandService;
run () {
let uri = new URI('/some/path/to/folder');
await this.commandService.executeCommand('vscode.openFolder', uri);
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 内置命令
在 OpenSumi 框架中,内置了许多基础命令,在需要实现时,你可以先到对应模块查找一下对应实现,避免重复劳动。常用的一些内置命令如下:
# 注册自定义命令
注册自定义命令的方式同样也存在两种方式:
# 通过插件注册
插件注册主要依赖 commands
贡献点,详细文档可见:contributes.commands (opens new window)。
在插件的 package.json
声明自定义命令的简单例子如下:
{
"contributes": {
"commands": [
{
"command": "extension.sayHello",
"title": "Hello World",
"category": "Hello",
"icon": {
"light": "path/to/light/icon.svg",
"dark": "path/to/dark/icon.svg"
}
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
声明的好处是能够让该命令“显式”的存在于框架中,即通过 ⇧⌘P
打开快速导航面板,或是在菜单中都能找到其位置,没有声明直接进行注册的命令将不会出现在上述面板中。
// sumi 自有插件 API
import * as sumi from 'sumi';
export async function activate(context: sumi.ExtensionContext) {
context.subscriptions.push(
sumi.commands.registerCommand('extension.sayHello', async () => {
sumi.window.showInformationMessage('Hello World');
})
);
}
2
3
4
5
6
7
8
9
10
# 通过模块注册
在模块中,我们通常采用 CommandContribution
进行注册,详细可见文档:命令注册 (opens new window)。
# 自定义快捷键
# 概览
作为一款富交互的 IDE,良好的快捷键设计能很大程度上解放使用者对于界面操作的依赖,提高工作/操作效率,而在 OpenSumi 框架中,除了支持通过插件的形式注册插件外,也支持通过模块的方式进行拓展,本文重点讲解如何在集成阶段为你的应用预设更多的快捷键。
# 注册快捷键
在模块中,我们通常采用 KeybindingContribution
的方式进行注册,详细可参见:快捷键注册 (opens new window)。
# 支持的快捷键字符
特定平台下支持的修饰符如下:
平台 | 修饰符 |
---|---|
macOS | Ctrl+ , Shift+ , Alt+ , Cmd+ |
Windows | Ctrl+ , Shift+ , Alt+ , Win+ |
Linux | Ctrl+ , Shift+ , Alt+ , Meta+ |
同时,你也可以在快捷键注册时使用 ctrlcmd
来作为修饰符使用,该修饰符在 macOS 下会被识别为 Cmd
而在 Linux 和 Windows 下会被识别为 Ctrl
。
其余支持的一些键值如下:
f1-f19
,a-z
,0-9
,
-,
=,
[,
],``,
;,
,,
,.
,/
left
,up
,right
,down
,pageup
,pagedown
,end
,home
tab
,enter
,escape
,space
,backspace
,delete
pausebreak
,capslock
,insert
numpad0-numpad9
,numpad_multiply
,numpad_add
,numpad_separator
numpad_subtract
,numpad_decimal
,numpad_divide
# 通过 when 控制生效范围
一般而言,在我们注册一个快捷键的时候,我们都只希望这个快捷键在特定的区域生效,通常我们建议使用 when
逻辑进行控制,在 OpenSumi 框架中定义了部分 when
表达式,大部分情况下你可以直接使用,见:contextkey/index.ts (opens new window)。
你只需要在注册快捷键的时候加上 when
字段,便可以让快捷键的只在 when
生效的时候被响应,这能有效避免在你的 IDE 中出现快捷键冲突的情况。如下:
keybindings.registerKeybinding({
command: 'type',
keybinding: 'enter',
when: 'editorTextFocus'
});
2
3
4
5
你也可以自定义或注册 when
表达式,详细可参考简单的 dialog.contextkey.ts (opens new window) 例子。
自此,你便可以通过集成自定义了快捷键的模块的方式实现对功能快捷键的定制。
# 自定义埋点上报
# 概览
OpenSumi 内置提供了埋点上报,给集成方提供一些关键 IDE 数据的指标、一方核心插件的一些关键性能等数据。集成方可自行将这些数据上报到自己的平台。
# 前端模块使用
import { IReporterService } from '@opensumi/ide-core-browser';
@Injectable()
class {
@Autowired(IReporterService)
reporterService: IReporterService
async activateExtension() {
...
const timer = reporterService.time(REPORT_NAME.ACTIVE_EXTENSION);
...
timer.timeEnd(extension.extensionId)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 后端模块使用
import { IReporterService } from '@opensumi/ide-core-node';
@Injectable()
class {
@Autowired(IReporterService)
reporterService: IReporterService
async activateExtension() {
...
reporterService.point(REPORT_NAME.ACTIVE_EXTENSION, extension.extensionId);
}
}
2
3
4
5
6
7
8
9
10
11
12
# 插件中使用
import { reporter } from 'sumi';
activate() {
...
const reporterTimer = reporter.time(`ts-load`);
func();
reporterTimer.timeEnd(`ts-load`);
}
function deactivate() {
reporter.dispose();
}
2
3
4
5
6
7
8
9
10
11
12
# 上报
集成方通过替换内置 Provider 实现:
import {
PointData,
PerformanceData,
IReporter
} from '@opensumi/ide-core-browser';
@Injectable()
class Reporter implements IReporter {
performance(name: string, data: PerformanceData) {}
point(name: string, data: PointData) {}
}
injector.addProviders({
token: IReporter,
useClass: Reporter
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 附录
# 框架内打点记录
# 自定义只读模式
# 概览
在一些特殊场景中,集成方希望可以在只读的模式下运行,例如 分享
功能,被分享的人只能看而不能写、不能使用某些命令、不能进行文件的创建和删除等这类需求。
那么我们可以利用 OpenSumi 的自定义模块能力,通过在模块当中禁用掉某些功能和 Command
命令来达到只读模式的效果。
# 自定义一个 readonly module
首先自定义一个 ReadonlyModule
模块:
@Injectable()
export class ReadonlyModule extends BrowserModule {
providers = [
// ... more contribution
ReadOnlyContribution
];
}
2
3
4
5
6
7
然后实现一个 ReadOnlyContribution
,并将其导入到 ReadonlyModule
的 providers:
@Domain(MenuContribution, CommandContribution, TabBarToolbarContribution)
export class ReadOnlyContribution
implements MenuContribution, CommandContribution, TabBarToolbarContribution {
@Autowired(IMenuRegistry)
protected menuRegistry: IMenuRegistry;
static UNREGISTER_COMMAND = new Set([
// 禁用文件保存
EDITOR_COMMANDS.SAVE_CURRENT,
EDITOR_COMMANDS.SAVE_URI,
EDITOR_COMMANDS.SAVE_ALL,
// 禁用文件操作
EDITOR_COMMANDS.NEW_UNTITLED_FILE,
FILE_COMMANDS.DELETE_FILE,
FILE_COMMANDS.RENAME_FILE,
FILE_COMMANDS.NEW_FILE,
FILE_COMMANDS.NEW_FOLDER,
FILE_COMMANDS.COPY_FILE,
FILE_COMMANDS.CUT_FILE,
FILE_COMMANDS.PASTE_FILE
]);
registerCommands(commands: CommandRegistry) {
// 卸载 command 逻辑
for (const command of ReadOnlyContribution.UNREGISTER_COMMAND) {
const cmd = typeof command === 'string' ? { id: command } : command;
commands.unregisterCommand(cmd);
}
}
registerMenus(menuRegistry: IMenuRegistry) {
// 只读模式下去掉 文件 和 编辑 两个 menu 菜单
menuRegistry.removeMenubarItem(MenuId.MenubarFileMenu);
menuRegistry.removeMenubarItem(MenuId.MenubarEditMenu);
}
registerToolbarItems(registry: ToolbarRegistry) {
registry.menuRegistry.removeMenubarItem(FILE_COMMANDS.NEW_FILE.id);
registry.menuRegistry.removeMenubarItem(FILE_COMMANDS.NEW_FOLDER.id);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
更多的 Command
(opens new window) 和 MenuId
(opens new window) 可在源码中查看,只需按照代码所示的位置卸载掉 command 或 menu 即可。
# 集成模块
最后在集成时引入,以 opensumi/ide-startup
案例为例子,参考 index.ts#L12 (opens new window) 时,导入到 modules 字段即可。
new ClientApp({
modules: [
// other modules
ReadonlyModule
],
// 还可以在默认配置这里设置 editor.forceReadOnly 为 true
defaultPreferences: {
'editor.forceReadOnly': true
}
});
2
3
4
5
6
7
8
9
10
自此你便完成了简单的只读模式的支持。
# 参考文档
- https://opensumi.com/zh/docs