sumi实践与分析
# Contribution替换部分
# 搜索替换
# 注册组建
将 ToolBar 组件进行注册,关联到一个字符串 Token test-toolbar
上:
两个要点:
/**
* 向侧边栏container内附加新的子视图
* @param view 子视图信息
* @param containerId 子视图需要附加的容器id
* @param props 初始prop
*/
collectViewComponent(view: View, containerId: string, props?: any, options?: ViewComponentOptions): string;
/**
* 替换一个已注册的视图
* @param view 子视图信息
* @param props 初始prop
*/
replaceViewComponent(view: View, props?: any): void;
2
3
4
5
6
7
8
9
10
11
12
13
示范:
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'
}
);
}
}
//自己注册编辑器页面
@Domain(ComponentContribution)
export class EditorContribution implements ComponentContribution {
registerComponent(registry: ComponentRegistry) {
registry.register("@opensumi/ide-editor", {
id: "ide-editor",
component: EditorView,
});
}
}
//完全自己自定义注册组件; Exporler方式;
@Domain(ComponentContribution)
export class DataDevComponent implements ComponentContribution {
registerComponent(registry: ComponentRegistry) {
// 目录树
registry.register(DATADEV_REGISTRY_ID, [], {
iconClass: getIcon("explorer"),
title: localize("datadev.title", "数据开发"),
priority: 999,
containerId: DATADEV_CONTAINER_ID,
badge: this.panelBadge, // 标记,
component: DataDevViewLeftPane,
activateKeyBinding: "ctrlcmd+shift+l",
});
}
}
[SlotLocation.left]: {
modules: [
"@opensumi/ide-explorer",
DATADEV_REGISTRY_ID,
],
},
//scm方式
registerComponent(registry: ComponentRegistry) {
registry.register('@opensumi/ide-scm', [], {
iconClass: getIcon('scm'),
title: localize('scm.title'),
priority: 8,
containerId: scmContainerId,
component: SCMViewContainer,
activateKeyBinding: 'ctrlcmd+shift+g',
});
}
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 卡槽替换
通过 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
# 编辑器页面替换
registerEditorComponent/registerResourceProvider
registerEditorComponent(registry: EditorComponentRegistry) {
// 将组件进行注册
registry.registerEditorComponent({
uid: COMPONENTS_ID,
scheme: COMPONENTS_SCHEME_ID,
component: AntdComponentsSampleView,
renderMode: EditorComponentRenderMode.ONE_PER_WORKBENCH,
});
// 将这个组件设置为这个 COMPONENTS_SCHEME_ID 的 resource 的默认打开方式
registry.registerEditorComponentResolver(COMPONENTS_SCHEME_ID, (resource, results) => {
results.push({
type: 'component',
componentId: COMPONENTS_ID,
});
});
}
registerResource(service: ResourceService) {
// 注册COMPONENTS_SCHEME_ID 可以在编辑器打开,并且设定对应的tab icon 和 名字
service.registerResourceProvider({
scheme: COMPONENTS_SCHEME_ID,
provideResource: async (uri: URI): Promise<IResource<any>> => {
const iconClass = this.iconService.fromIcon(
'',
'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
IconType.Background,
);
return {
uri,
name: 'AntD 组件案例',
icon: iconClass!,
}
},
});
}
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
# 菜单相关id
export declare enum MenuId {
AccountsContext = "accounts/context",
ActivityBarContext = "activityBar/context",
ActivityBarExtra = "activityBar/extra",
CommandPalette = "commandPalette",
DebugBreakpointsContext = "debug/breakpoints/context",
DebugCallStackContext = "debug/callstack/context",
DebugConsoleContext = "debug/console/context",
DebugVariablesContext = "debug/variables/context",
DebugWatchContext = "debug/watch/context",
DebugToolBar = "debug/toolbar",
EditorContext = "editor/context",
EditorTitle = "editor/title",
EditorTitleContext = "editor/title/context",
EmptyEditorGroupContext = "empty/editor/group/context",
ExplorerContext = "explorer/context",
MenubarAppearanceMenu = "menubar/appearance",
MenubarAppMenu = "menubar/app",
MenubarDebugMenu = "menubar/debug",
MenubarEditMenu = "menubar/edit",
MenubarFileMenu = "menubar/file/menu",
MenubarGoMenu = "menubar/go",
MenubarHelpMenu = "menubar/help",
MenubarLayoutMenu = "menubar/layout",
MenubarNewBreakpointMenu = "menubar/new/breakpoint",
MenubarPreferencesMenu = "menubar/preferences",
MenubarRecentMenu = "menubar/recent",
MenubarSelectionMenu = "menubar/selection",
MenubarSwitchEditorMenu = "menubar/switch/editor",
MenubarSwitchGroupMenu = "menubar/switch/group",
MenubarTerminalMenu = "menubar/terminal",
MenubarViewMenu = "menubar/view",
TerminalInstanceContext = "terminal/instance/context",
TerminalNewDropdownContext = "terminal/newDropdown/context",
TerminalTabContext = "terminal/tab/context",
TerminalPanelContext = "terminal/panel/context",
OpenEditorsContext = "open/editors/context",
ProblemsPanelContext = "problems/panel/context",
SCMChangeTitle = "scm/change/title",
SCMResourceContext = "scm/resourceState/context",
SCMResourceGroupContext = "scm/resourceGroup/context",
SCMResourceFolderContext = "scm/resourceFolder/context",
SCMSourceControl = "scm/sourceControl",
SCMTitle = "scm/title",
SCMInput = "scm/input",
SearchContext = "search/context",
StatusBarContext = "statusbar/context",
StatusBarWindowIndicatorMenu = "statusbar/windowIndicator",
TouchBarContext = "touchBar/context",
ViewItemContext = "view/item/context",
ViewTitle = "view/title",
GlobalActivity = "global/activity",
ExtensionContext = "extension/context",
SettingsIconMenu = "settings/icon/menu",
CommentsCommentThreadContext = "comments/commentThread/context",
CommentsCommentThreadTitle = "comments/commentThread/title",
CommentsCommentTitle = "comments/comment/title",
CommentsCommentContext = "comments/comment/context",
KTToolbarLocationContext = "kt/toolbar/context",
MarketplaceNoResultsContext = "marketplace/noResults/context",
TestingGlyphMarginContext = "testing/glyphMargin/context",
TestPeekTitleContext = "testing/outputPeek/title/context"
}
export declare function getTabbarCommonMenuId(location: string): string;
//# sourceMappingURL=menu-id.d.ts.map
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# ViewState
源码部分
export interface ViewState {
width: number;
height: number;
}
export const useViewState = (
location: string,
containerRef: React.MutableRefObject<HTMLElement | null | undefined>,
manualObserve?: boolean,
): ViewState => {
const eventBus = useInjectable<IEventBus>(IEventBus);
const [viewState, setViewState] = React.useState({ width: 0, height: 0 });
const viewStateRef = React.useRef<ViewState>(viewState);
React.useEffect(() => {
let lastFrame: number | null;
const disposer = eventBus.on(ResizeEvent, (e) => {
if (!manualObserve && e.payload.slotLocation === location) {
if (lastFrame) {
window.cancelAnimationFrame(lastFrame);
}
lastFrame = window.requestAnimationFrame(() => {
if (containerRef.current && containerRef.current.clientHeight && containerRef.current.clientWidth) {
setViewState({ height: containerRef.current.clientHeight, width: containerRef.current.clientWidth });
}
});
}
});
return () => {
disposer.dispose();
};
}, [containerRef.current]);
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
# 通信模块(connection)
基于 jsonrpc 完成多端远程调用场景,兼容 lsp 等通信方式
# opensumi 中使用
准备
- 框架中服务分为运行在浏览器环境的 前端服务(frontService) 与运行在 node 环境的 后端服务(backService),服务在两端的实现方式是一致的
- 目前在
tools/dev-tool
中的启动逻辑中完成了服务的注册和获取逻辑,在具体功能模块中无需关心具体的通信注册获取逻辑
后端服务(backService) 后端服务(backService) 即在 Web Server 暴露的能力,类似 web 应用框架中 controller 提供的请求响应逻辑
- 注册服务
packages/file-service/src/node/index.ts
import { FileSystemNodeOptions, FileService } from './file-service';
import { servicePath } from '../common/index';
export class FileServiceModule extends NodeModule {
providers = [{ token: 'FileServiceOptions', useValue: FileSystemNodeOptions.DEFAULT }];
backServices = [
{
servicePath,
token: FileService,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
例如在 file-service 模块中,通过定义 backServices
数组,传递模块提供的后端服务,servicePath
为前端模块引用的服务地址,以及对应服务的注入 token
- 服务调用
packages/file-tree/src/browser/index.ts
import { servicePath as FileServicePath } from '@opensumi/ide-file-service';
@Injectable()
export class FileTreeModule extends BrowserModule {
providers: Provider[] = [createFileTreeAPIProvider(FileTreeAPIImpl)];
backServices = [
{
servicePath: FileServicePath,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
例如在 file-tree 模块中,首先在模块入口位置声明需要用到的 backServices
,传入引用的服务 servicePath
,与服务注册时的 servicePath
一致
packages/file-tree/src/browser/file-tree.service.ts
import {servicePath as FileServicePath} from '@opensumi/ide-file-service';
@Injectable()
export default class FileTreeService extends Disposable {
@observable.shallow
files: CloudFile[] = [];
@Autowired()
private fileAPI: FileTreeAPI;
@Autowired(CommandService)
private commandService: CommandService;
constructor(@Inject(FileServicePath) protected readonly fileService) {
super();
this.getFiles();
}
createFile = async () => {
const {content} = await this.fileService.resolveContent('/Users/franklife/work/ide/ac/ide-framework/tsconfig.json');
const file = await this.fileAPI.createFile({
name: 'name' + Date.now() + '\n' + content,
path: 'path' + Date.now(),
});
// 只会执行注册在 Module 里声明的 Contribution
this.commandService.executeCommand('file.tree.console');
if (this.files) {
this.files.push(file);
} else {
this.files = [file];
}
}
}
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
在 file-tree.service.ts 中通过 servicePath
进行注入,并直接调用在服务类上的方法
constructor(@Inject(FileServicePath) protected readonly fileService) {
super();
this.getFiles();
}
2
3
4
5
方法调用会转换成一个远程调用进行响应,返回结果
const { content } = await this.fileService.resolveContent('/Users/franklife/work/ide/ac/ide-framework/tsconfig.json');
前端服务(frontService) 后端服务(backService) 即在 Browser 环境下运行的代码暴露的能力
- 注册服务
packages/file-ree/src/browser/index.ts
@Injectable()
export class FileTreeModule extends BrowserModule {
providers: Provider[] = [createFileTreeAPIProvider(FileTreeAPIImpl)];
backServices = [
{
servicePath: FileServicePath,
},
];
frontServices = [
{
servicePath: FileTreeServicePath,
token: FileTreeService,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
与后端服务注册类似,例如在 file-tree 模块中声明 frontServices
字段,传入对应的服务地址 servicePath
和对应服务的注入 token
- 服务使用
packages/file-service/src/node/index.ts
import { servicePath as FileTreeServicePath } from '@opensumi/ide-file-tree';
@Injectable()
export class FileServiceModule extends NodeModule {
providers = [{ token: 'FileServiceOptions', useValue: FileSystemNodeOptions.DEFAULT }];
backServices = [
{
servicePath,
token: FileService,
},
];
frontServices = [
{
servicePath: FileTreeServicePath,
},
];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
与使用后端服务一致,在模块定义中声明需要使用的前端服务 frontServices
,传入前端服务注册时用的 servicePath
一致
packages/file-service/src/node/file-service.ts
@Injectable()
export class FileService implements IFileService {
constructor(
@Inject('FileServiceOptions') protected readonly options: FileSystemNodeOptions,
@Inject(FileTreeServicePath) protected readonly fileTreeService
) { }
async resolveContent(uri: string, options?: { encoding?: string }): Promise<{ stat: FileStat, content: string }> {
const fileTree = await this.fileTreeService
fileTree.fileName(uri.substr(-5))
...
return { stat, content };
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
与使用后端服务使用方式一致,在 file-service.ts 中通过 servicePath
进行注入,通过调用注入服务的对应方法
constructor(
@Inject('FileServiceOptions') protected readonly options: FileSystemNodeOptions,
@Inject(FileTreeServicePath) protected readonly fileTreeService
) { }
2
3
4
方法调用会转换成一个远程调用进行响应,返回结果
const fileTree = await this.fileTreeService;
fileTree.fileName(uri.substr(-5));
2
与后端服务调用区别的是,目前因前端代码后置执行,所以首先需要获取服务 await this.fileTreeService
后进行调用