贡献点(contribution)
@opensumi/ide-xxx 提供向各个模块提供贡献点的数据结构,如果想为某个模块提供贡献点,要做的是实现这个模块的数据结构,完成自己的逻辑,然后把自己实现的这个 class 注册到 extends BrowserModule 中的class 中。
比如:
所有向编辑器模块贡献功能的贡献点统一使用 BrowserEditorContribution
要写一个 contributions,需要实现以下的方法
typescript
interface BrowserEditorContribution {
/**
* 用来在合适的时机向 `ResourceService` 注册可以在编辑器内打开的资源。
*
* 为了让一个 uri 能够在编辑器中被打开,首先需要向 `ResourceService` 注册一个用于解析 uri 至一个编辑器资源(`IResource`) 的 `IResourceProvider`。
* 它的主要职责是在这个 uri 在编辑器标签 Tab 上显示时提供它的名称、图标、是否被编辑等状态,以及相应这个 tab 被关闭时的回调等等。
*
* @param resourceService
*/
registerResource?(resourceService: ResourceService): void;
/**
* 用来在合适的时机向 `EditorComponentRegistry` 注册编辑器组件、打开方式等功能。
*
* 一个 uri 对应的编辑器资源 (`IResource`) 需要能够在编辑器中展示,还需要为它注册对应的一个或者多个打开方式,以及对应打开方式使用的 React 组件。
* @param editorComponentRegistry
*/
registerEditorComponent?(editorComponentRegistry: EditorComponentRegistry): void;
registerEditorDocumentModelContentProvider?(registry: IEditorDocumentModelContentRegistry): void;
/**
* @deprecated
* @param editorActionRegistry
*/
registerEditorActions?(editorActionRegistry: IEditorActionRegistry): void;
/**
* 当进入 IDE 时,编辑器会尝试恢复上一次打开的编辑器组和组内打开的文件
* 完成后会执行 onDidRestoreState 这个 hook
*/
onDidRestoreState?(): void;
registerEditorFeature?(registry: IEditorFeatureRegistry): any;
}
比如说注册可以在编辑器打开的资源,要实现 ResourceService 一个抽象类。
typescript
declare abstract class ResourceService {
/**
* 注册一个新的 ResourceProvider 会触发该事件
*/
readonly onRegisterResourceProvider: Event<IResourceProvider>;
/**
* 写在一个 ResourceProvider 会触发该事件
*/
readonly onUnregisterResourceProvider: Event<IResourceProvider>;
/**
* 根据uri获得一个资源信息
* 如果uri没有对应的resource提供者,则会返回null
* @param uri
*/
abstract getResource(uri: URI): Promise<IResource | null>;
/**
* 注册一个resource提供方
* @param provider
*/
abstract registerResourceProvider(provider: IResourceProvider): IDisposable;
/**
* 是否能关闭一个资源
*/
abstract shouldCloseResource(resource: IResource, openedResources: IResource[][]): Promise<boolean>;
abstract getResourceDecoration(uri: URI): IResourceDecoration;
abstract getResourceSubname(resource: IResource, groupResources: IResource[]): string | null;
/**
* 销毁一个 resource
* @param resource
*/
abstract disposeResource(resource: IResource<any>): void;
/**
* 是否存在 provider 可以处理某个 uri
*/
abstract handlesUri(uri: URI): boolean;
}
registerResource(resourceService: ResourceService) {
// 处理 git 协议的 editor tab 展示信息
resourceService.registerResourceProvider(xxxProvider);
}
要注册的资源有一个别名叫 Provider(可以理解提供资源的一方,资源 == Provider)
xxxProvider 就是要注册的资源,provider 实现了 IResourceProvider
typescript
interface IResourceProvider {
scheme?: string;
/**
* 一个 provider 是否处理某个资源
* 返回优先级,这个值越高的 provider 越优先处理, 小于 0 表示不处理
* 这个比较的计算结果会被缓存,仅仅当 provider 数量变更时才会清空
* 存在 handlesURI 时, 上面的scheme会被忽略
*/
handlesUri?(uri: URI): number;
provideResource(uri: URI): MaybePromise<IResource>;
provideResourceSubname?(resource: IResource, groupResources: IResource[]): string | null;
shouldCloseResource?(resource: IResource, openedResources: IResource[][]): MaybePromise<boolean>;
onDisposeResource?(resource: IResource): void;
}
registerEditorDocumentModelContentProvider 注册 git content provider provider 提供 doc / 文档的内容和 meta 信息
registerEditorDocumentModelContentProvider(registry: IEditorDocumentModelContentRegistry) {
// 注册 git content provider provider 提供 doc / 文档的内容和 meta 信息
registry.registerEditorDocumentModelContentProvider(xxxProvider);
}
此时的 xxxProvider 也是一种资源,注入到 EditorDocumentModelContent 中
xxxProvider 是对 IEditorDocumentModelContentProvider 的实现
php
interface IEditorDocumentModelContentProvider {
/**
* 是否处理这个Scheme的uri
* 权重等级等同于 handlesUri => 10
* @param scheme
*/
handlesScheme?(scheme: string): MaybePromise<boolean>;
/**
* 处理一个URI的权重, -1表示不处理, 如果存在handlesUri, handlesScheme将被忽略
* @param scheme
*/
handlesUri?(uri: URI): MaybePromise<number>;
/**
* 提供文档内容
* @param uri
* @param encoding 以某种编码获取内容
*/
provideEditorDocumentModelContent(uri: URI, encoding?: string): MaybePromise<string>;
/**
* 这个文档是否只读(注意只读和无法保存的区别)
* @param uri
*/
isReadonly(uri: URI): MaybePromise<boolean>;
/**
* 保存一个文档, 如果不存在这个方法,那这个文档无法被保存
* 当文档无法保存时, 文档永远不会进入dirty状态,并且来自provider的
* @param uri
* @param content
* @param baseContent dirty前的内容
* @param ignoreDiff 无视diff错误, 强行覆盖保存
*/
saveDocumentModel?(uri: URI, content: string, baseContent: string, changes: IEditorDocumentChange[], encoding?: string, ignoreDiff?: boolean, eol?: EOL): MaybePromise<IEditorDocumentModelSaveResult>;
/**
* 为一个uri提供喜好的语言id,返回undefined则交由编辑器自己去判断
* @param uri
*/
preferLanguageForUri?(uri: URI): MaybePromise<string | undefined>;
provideEOL?(uri: URI): MaybePromise<EOL>;
/**
* 为一个uri提供encoding信息, 如果不实现,则默认UTF-8
* @param uri;
*/
provideEncoding?(uri: URI): MaybePromise<string>;
/**
* 提供这个文件当前内容的md5值,如果不实现这个函数,会使用content再执行计算
* @param uri
*/
provideEditorDocumentModelContentMd5?(uri: URI, encoding?: string): MaybePromise<string | undefined>;
/**
* 文档内容变更事件,交由modelManager决定是否处理
*/
onDidChangeContent: Event<URI>;
onDidDisposeModel?(uri: URI): void;
/**
* 是否永远显示 dirty
* 有些类型的文档(untitled)可能刚创建就是 dirty,允许它以空文件的状态保存
*/
isAlwaysDirty?(uri: URI): MaybePromise<boolean>;
/**
* 是否关闭自动保存功能
*/
closeAutoSave?(uri: URI): MaybePromise<boolean>;
/**
* 猜测编码
*/
guessEncoding?(uri: URI): Promise<string | undefined>;
}
用一个抽象类来实现 IEditorDocumentModelContentProvider,抽象类中有一个抽象方法。
如何实现抽象方法?
scala
@Injectable()
export class IndexModule extends BrowserModule {
providers: Provider[] = [
SchemeContribution,
ChangesTreeContribution,
CodeReviewContribution,
DiffFoldingContribution,
{
token: AbstractSCMDocContentProvider, // 以抽象类为token
useClass: AoneGitDocContentProvider, // 把具体的实现放在 useClass 中,后续xxxProvider 的方法调用来自于 useClass 中的类方法(好特么绕)
}
];
}
题外话: 代码通过 useClass 的方式把 Provider 的具体行为传入。行为中又灌入了 api Injectable,这样 GitDocContentProvider 可以使用 api.XXXX 来进行接口调用,到 api 这层,就是业务方自定义了(真特么的绕啊...)
题外话2:registerToolbarItems 中,注册 toolbar 的 item,registerItem 中参数的数据结构如下。
字段 when 表示的是是否在视图层展示。
php
interface ICoreMenuItem {
order?: number;
/**
* 决定是否在视图层展示
*/
when?: string | ContextKeyExpr; // not true/false !!!
/**
* 单独变更此 menu action 的 args
*/
argsTransformer?: ((...args: any[]) => any[]);
}
// usage
{
id: ChangesTreeViewId,
when: '!sourceMode' // 通过表达式得到 true/false 的结果。有点像 sql 语法
}
再比如:文件变更树。需要用到组件/命令/toolbar,则需要implements ComponentContribution, CommandContribution, TabBarToolbarContribution
// usage
@Domain(ComponentContribution, CommandContribution, TabBarToolbarContribution)
export class ChangesTreeContribution implements ComponentContribution, CommandContribution, TabBarToolbarContribution {
// MR Explorer 只注册容器
registerComponent(registry: ComponentRegistry) {
registry.register(
'mr-explorer',
[
{
id: ChangesTreeViewId,
name: localize('changes.tree', '变更信息'),
weight: 5,
priority: 8, // 按权重 决定排列的顺序
collapsed: false,
component: ChangesTreeView, // **注册自己写的组件**
when: '!sourceMode'
}
],
{
title: localize('mr.explorer', '代码变更'),
iconClass: getIcon('PR'),
priority: 9,
containerId: 'mr-explorer',
},
);
}
}
// 如果要注册命令,则需要 implements CommandContribution
@Domain(CommandContribution)
export class xxCommandContribution implements CommandContribution {
registryCommands(registry: CommandRegistry): void {
commands.registerCommand(command, { //command 为复合结构, { id: 唯一标识,...others 非必需}
execute: () => {
// do yourself code...
},
});
}
}
// 同理,如果需要对某个tab的 toolbar 添加小部件,需要 implements TabBarToolbarContribution
@Domain(TabBarToolbarContribution)
export class xxCommandContribution implements TabBarToolbarContribution {
registerToolbarItems(registry: ToolbarRegistry) {
registry.registerItem({
id: ChangesTreeCommands.ToggleTree.id, // 如果想使用命令达到同样的效果,需要配置已注册的 command
command: ChangesTreeCommands.ToggleTree.id, //应该是用 registryCommand 中的command 命令,这样写不是跟id一样??
viewId: ChangesTreeViewId, // 要注册到哪个 tab 上去,需要标志 tab 的 id
label: localize('codereview.tree.showTreeModel', '树形模式'),
toggledWhen: 'changes-tree-mode', //全局注册的上下文 key globalContextKeyService
order: 4, // toolbar 的顺序
});
}
看到这儿相信你一定会有这种感觉:opensumi 只是一个框架,它只负责对外提供接口和生命周期,具体要实现一个编辑器,需要自己来完成,像 vue 框架一样,语法(数据结构)已经给定,至于是杀猪还是煮菜端看业务方如何使用。(Yep,我又在说废话了,手动狗头:)
service 一般为 Injectable 和 @Autowired
链路真的太长太长了,看着看着不知道看到哪里了
业务逻辑
文件变更树:ChangesTreeContribution
kotlin
class ChangesTreeContribution implements ComponentContribution, CommandContribution, TabBarToolbarContribution
从 changesTreeModelService 中结构出来 decorationService / labelService / commandService,注入到 ChangeTreeNode 组件中
changeTree 本质上是用 FilterableRecycleTree(RecycleTreeFilterDecorator(RecycleTree)) 组件实现
编辑器主干区域:CodeReviewContribution
class CodeReviewContribution extends WithEventBus implements CommandContribution, ClientAppContribution, CommentsContribution, NextMenuContribution, ComponentContribution, BrowserEditorContribution, KeybindingContribution, SlotRendererContribution
依赖 8 个 contribution