Skip to content

Commit c801052

Browse files
committed
[Feat] menuRegistry
1 parent a0d3c82 commit c801052

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed

3. Systems/.image/menu1.png

35.8 KB
Loading

3. Systems/.image/menu2.png

59.3 KB
Loading
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# VSCode系列「系统篇」:深入理解 MenuRegistry
2+
3+
## 背景介绍
4+
一个软件中,「右键菜单」是非常平凡的一个操作:
5+
1. 你需要编辑一个文件/文件夹,你就右键它弹出针对文件树的文件/文件夹内容的「右键菜单」。
6+
2. 你需要编辑一段文字,你选中并右键它,会弹出一个针对文本内容的「右键菜单」。
7+
3. 你需要编辑一个xxx,你就右键它,会弹出一个针对xxx内容的「右键菜单」。
8+
4. ...
9+
10+
> **一个软件内部,针对不同的「右键对象」,会弹出不同的「右键菜单」。** 比方说,VSCode整个软件里,一共有189个不同内容的「右键菜单」。
11+
12+
VSCode这么多「右键菜单」,它是怎么统一管理操作逻辑,统一修改内容的呢?
13+
14+
## VSCode:`MenuRegistry``MenuId`
15+
16+
VSCode把这种「**右键菜单****里所呈现的内容**在代码里面称呼为`Menu`
17+
我第一次意识到VSCode有统一管理`Menu`里的内容的倾向是看到了源码里有两个重要的工具叫做:
18+
1. `MenuRegistry`
19+
2. `MenuId`
20+
21+
`MenuRegistry``MenuId`都隶属于同一个文件:`src\vs\platform\actions\common\actions.ts`(不得不吐槽这个文件的名字真的感觉跟它没什么血缘关系。。。)`MenuRegistry`它本质就是一个全局变量,用来储存注册信息的,仅此而已。它的API如下:
22+
```ts
23+
export interface IMenuRegistry {
24+
readonly onDidChangeMenu: Event<IMenuRegistryChangeEvent>;
25+
addCommand(userCommand: ICommandAction): IDisposable;
26+
getCommand(id: string): ICommandAction | undefined;
27+
getCommands(): ICommandsMap;
28+
29+
/**
30+
* @deprecated Use `appendMenuItem` or most likely use `registerAction2` instead. There should be no strong
31+
* reason to use this directly.
32+
*/
33+
appendMenuItems(items: Iterable<{ id: MenuId; item: IMenuItem | ISubmenuItem; }>): IDisposable;
34+
appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable;
35+
getMenuItems(loc: MenuId): Array<IMenuItem | ISubmenuItem>;
36+
}
37+
```
38+
## VSCode: `MenuRegistry``MenuId`的使用案例
39+
这里面的`addCommand`系列不是很重要,所以不属于今天讨论的话题,~~当然不是我读不懂是干嘛~~的。主要看`appendMenuItem`。我们来看看VSCode源码里是怎么用这个API的:
40+
```ts
41+
// Menu registration - explorer
42+
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
43+
group: 'navigation',
44+
order: 4,
45+
command: {
46+
id: NEW_FILE_COMMAND_ID,
47+
title: NEW_FILE_LABEL,
48+
precondition: ExplorerResourceNotReadonlyContext
49+
},
50+
when: ExplorerFolderContext
51+
});
52+
// ...
53+
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
54+
group: '3_compare',
55+
order: 30,
56+
command: compareSelectedCommand,
57+
when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, WorkbenchListDoubleSelection)
58+
});
59+
// ...
60+
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
61+
group: '5_cutcopypaste',
62+
order: 10,
63+
command: {
64+
id: COPY_FILE_ID,
65+
title: COPY_FILE_LABEL,
66+
},
67+
when: ExplorerRootContext.toNegated()
68+
});
69+
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
70+
group: '5_cutcopypaste',
71+
order: 20,
72+
command: {
73+
id: PASTE_FILE_ID,
74+
title: PASTE_FILE_LABEL,
75+
precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext, FileCopiedContext)
76+
},
77+
when: ExplorerFolderContext
78+
});
79+
// a bunch of all other registrations...
80+
```
81+
我只列出来4条注册信息,其实这个文件里还注册了非常多其他的功能。其中`MenuId.ExplorerContext`其实所指代的就是文件树里的「右键菜单」。最终在VSCode里呈现的效果如下图所示:
82+
![alt text](./.image/menu1.png)
83+
我来总结一下这4条在干什么:
84+
1. 4条注册信息都在往一个叫做`MenuId.ExplorerContext``MenuId`里注册一条item。每一条item其实就是「右键菜单」的一行按钮。
85+
2. 比如第一条注册信息,就是注册了一个名字(title)叫做`NEW_FILE_LABEL`,点击按钮之后会运行一个命令(command)叫做`NEW_FILE_COMMAND_ID`。这行按钮的group叫做`navigation`,在渲染的时候会和其他被标记为`navigation`的item一起渲染在一起,然后画一个横线和其他的group分开。而`when`里填写的则是告诉软件这一行item要在`ExplorerFolderContext`条件为true的情况下才渲染。
86+
87+
具体的注册API`appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable`其中的类型定义如下:
88+
```ts
89+
export interface IMenuItem {
90+
command: ICommandAction;
91+
alt?: ICommandAction;
92+
/**
93+
* Menu item is hidden if this expression returns false.
94+
*/
95+
when?: ContextKeyExpression;
96+
group?: 'navigation' | string;
97+
order?: number;
98+
isHiddenByDefault?: boolean;
99+
}
100+
export interface ISubmenuItem {
101+
title: string | ICommandActionTitle;
102+
submenu: MenuId;
103+
icon?: Icon;
104+
when?: ContextKeyExpression;
105+
group?: 'navigation' | string;
106+
order?: number;
107+
isSelection?: boolean;
108+
// for dropdown menu: if true the last executed action is remembered as the default action
109+
rememberDefaultAction?: boolean;
110+
}
111+
```
112+
## `MenuId`
113+
`MenuId`的定义如下:
114+
```ts
115+
export class MenuId {
116+
117+
private static readonly _instances = new Map<string, MenuId>();
118+
119+
static readonly CommandPalette = new MenuId('CommandPalette');
120+
static readonly DebugBreakpointsContext = new MenuId('DebugBreakpointsContext');
121+
static readonly DebugCallStackContext = new MenuId('DebugCallStackContext');
122+
static readonly DebugConsoleContext = new MenuId('DebugConsoleContext');
123+
static readonly DebugVariablesContext = new MenuId('DebugVariablesContext');
124+
// ...other 180+ menus
125+
}
126+
```
127+
其实这个`MenuId`吧,虽然在VSCode源码里写成了class形式,但我觉得换成`const Enum`,换成`Symbol`之类的也完全可以。class本身的prototype并没有提供任何有用的API。就是`new MenuId(SOME_STRING_ID)`然后拿返回值当作unique identifier使用。
128+
129+
## 如何实现`SubMenu`功能
130+
以下代码来自`src\vs\editor\contrib\clipboard\browser\clipboard.ts`:
131+
```ts
132+
// ...
133+
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { submenu: MenuId.ExplorerContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 });
134+
// ...
135+
```
136+
其中argument中填入了`submenu: MenuId.ExplorerContextShare`。意思就是这里将会渲染一行叫做`share`的按钮,然后它在UI的呈现上是一个submenu的形式。如刚刚较早呈现的图片里所示(你能看到share按钮右边有个submenu的指示器)。
137+
138+
那么你该如何往这个叫做`share``Submenu`里注册item呢?
139+
* 非常简单,就如同上方是如何往`MenuId.ExplorerContext`里注册items一样,你也应该用同样的方法调用`MenuRegistry.addMenuItem()``MenuId.ExplorerContextShare`注册items。
140+
* 在UI渲染的时候,会去检测该menu下的每一个item是否属于submenu,我们注册menu的时候只需要标记一下即可。
141+
142+
## `MenuRegistry`不仅仅是为了「右键菜单」服务的
143+
144+
这里我得提一个在vscode源码中大量使用的一个函数,据目前写文章位置,这个函数在源码中出现了`1303`次。
145+
函数定义如下:
146+
```ts
147+
export function registerAction2(ctor: { new(): Action2; }): IDisposable {
148+
const disposables: IDisposable[] = []; // not using `DisposableStore` to reduce startup perf cost
149+
const action = new ctor();
150+
const { f1, menu, keybinding, ...command } = action.desc;
151+
// command ...
152+
// menu
153+
if (Array.isArray(menu)) {
154+
for (const item of menu) {
155+
disposables.push(MenuRegistry.appendMenuItem(item.id, { command: { ...command, precondition: item.precondition === null ? undefined : command.precondition }, ...item }));
156+
}
157+
158+
} else if (menu) {
159+
disposables.push(MenuRegistry.appendMenuItem(menu.id, { command: { ...command, precondition: menu.precondition === null ? undefined : command.precondition }, ...menu }));
160+
}
161+
if (f1) {
162+
disposables.push(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: command.precondition }));
163+
disposables.push(MenuRegistry.addCommand(command));
164+
}
165+
166+
// keybinding
167+
if (Array.isArray(keybinding)) {
168+
// ...
169+
} else if (keybinding) {
170+
// ...
171+
}
172+
// ...
173+
}
174+
```
175+
* 其中有一行代码是`MenuRegistry.appendMenuItem(MenuId.CommandPalette, ...)`, 也就是说每注册一次action,在条件允许的情况下(`f1`被定义了)都会往`MenuId.CommandPalette`注册一条。而`CommandPalette`就是下图这个功能(可以通过`ctrl+shift+P`快捷键调出来):
176+
![alt text](./.image/menu2.png)
177+
* 这里就能看出来,`MenuRegistry`里的注册信息,除了可以为「右键菜单」的渲染服务,也可以为了任何具有「列表性质」的UI服务。所以这个`MenuRegistry`的泛用性极其广泛。

0 commit comments

Comments
 (0)