Contribution Model & Registry
← Về MOC · Trước: Dependency Injection & Services
1. Ý tưởng
Workbench không phụ thuộc trực tiếp vào các tính năng như search, git, debug. Thay vào đó, mỗi tính năng tự “đóng góp” (contribute) vào workbench bằng cách đăng ký vào các registry (sổ đăng ký trung tâm). Đây là cơ chế nội bộ, khác với Extension API công khai (Extension Host & RPC).
Mã nằm ở src/vs/workbench/contrib/ (101 nhóm tính năng).
2. Quy tắc của thư mục contrib (theo wiki chính thức)
- Không có phụ thuộc từ bên ngoài
contrib/vào bên trongcontrib/. - Mỗi tính năng có một file
.contribution.tslàm điểm vào, ví dụcontrib/search/browser/search.contribution.ts. File này được import từ entry point (Môi trường đích (Target Environments)). - Mỗi tính năng công bố API nội bộ qua một file common duy nhất, ví dụ
contrib/search/common/search.ts. - Tính năng A được dùng API common của tính năng B, nhưng không được với vào bên trong (internals) của B.
- Service chỉ dùng trong một contrib → đăng ký ngay trong file
.contribution.tscủa contrib đó.
3. Các registry thường gặp
| Registry | Đăng ký cái gì | Ví dụ API |
|---|---|---|
Registry (chung) | Kho tổng chứa các registry con | Registry.as<T>(id) – src/vs/platform/registry/common/platform.ts |
| Configuration registry | Setting (cài đặt) hiển thị trong Settings | registerConfiguration({...}) |
| Views registry | View (khung hiển thị) trong sidebar/panel | registerViews([...], container) |
CommandsRegistry | Command (lệnh) theo ID | registerCommand(id, handler) |
MenuRegistry | Mục menu (menu chuột phải, thanh tiêu đề view…) | appendMenuItem(MenuId.X, {...}) |
registerAction2 | Gộp command + menu + keybinding (phím tắt) trong 1 class | registerAction2(class extends Action2 {...}) |
| Workbench contributions | Class tự chạy ở một giai đoạn khởi động | registerWorkbenchContribution2(id, ctor, phase) |
| Icon registry | Icon có thể tùy biến theo theme | registerIcon(id, Codicon.x, mô tả) |
4. Phân tích ví dụ thật: tính năng Timeline
Cấu trúc thư mục:
src/vs/workbench/contrib/timeline/
├── common/
│ ├── timeline.ts ← API common: ITimelineService, kiểu dữ liệu
│ └── timelineService.ts ← hiện thực service
└── browser/
├── timeline.contribution.ts ← điểm vào: view, setting, command, menu
├── timeline.service.contribution.ts ← đăng ký service
└── timelinePane.ts ← giao diện view4.1 Đăng ký setting
// trích timeline.contribution.ts (thêm chú thích)
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
id: 'timeline',
title: localize('timelineConfigurationTitle', "Timeline"), // chuỗi hiển thị phải localize
type: 'object',
properties: {
'timeline.pageSize': { // tên setting người dùng thấy trong settings.json
type: ['number', 'null'],
default: 50,
markdownDescription: localize('timeline.pageSize', "The number of items to show ..."),
},
}
});Kết quả: trong settings.json của người dùng có thể ghi "timeline.pageSize": 100.
4.2 Đăng ký view
export class TimelinePaneDescriptor implements IViewDescriptor {
readonly id = TimelinePaneId; // 'timeline'
readonly ctorDescriptor = new SyncDescriptor(TimelinePane); // tạo TimelinePane khi cần (qua DI)
readonly collapsed = true;
readonly canMoveView = true;
// ...
}
// Gắn view vào container của Explorer
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([new TimelinePaneDescriptor()], VIEW_CONTAINER);
SyncDescriptorLà “bản mô tả cách tạo” một class. View chưa được tạo ngay; khi người dùng mở view, workbench dùng
IInstantiationServicetạoTimelinePanevà tự tiêm service (xem Dependency Injection & Services).
4.3 Đăng ký command và menu
CommandsRegistry.registerCommand('files.openTimeline', OpenTimelineAction.handler());
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { // menu chuột phải trong Explorer
group: '4_timeline',
order: 1,
command: { id: 'files.openTimeline', title: localize('files.openTimeline', "Open Timeline") },
// Chỉ hiện khi: đang chọn một resource VÀ có timeline provider
when: ContextKeyExpr.and(ResourceContextKey.HasResource, TimelineHasProviderContext)
});Context key và mệnh đề
whenContext key (khóa ngữ cảnh) là biến trạng thái toàn cục (ví dụ
editorTextFocus,resourceExtname). Mệnh đềwhenlà biểu thức logic trên các context key, quyết định menu/phím tắt có hiệu lực hay không. Extension cũng dùng cùng cơ chế này trongpackage.json(xem Thực hành: viết extension đầu tiên). Mã nguồn:src/vs/platform/contextkey/.
5. registerAction2 – cách hiện đại
Gộp định nghĩa lệnh, menu, phím tắt vào một class:
// trích src/vs/workbench/contrib/outline/browser/outlineActions.ts (thêm chú thích)
registerAction2(class CollapseAll extends ViewAction<IOutlinePane> {
constructor() {
super({
viewId: IOutlinePane.Id,
id: 'outline.collapse', // ID lệnh
title: localize('collapse', "Collapse All"), // nhãn hiển thị
f1: false, // false = không hiện trong Command Palette (F1)
icon: Codicon.collapseAll,
menu: {
id: MenuId.ViewTitle, // nút trên thanh tiêu đề view
group: 'navigation',
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', IOutlinePane.Id), ctxAllCollapsed.isEqualTo(false))
}
});
}
runInView(_accessor: ServicesAccessor, view: IOutlinePane) {
view.collapseAll(); // hành động thực tế
}
});6. Workbench contribution và giai đoạn khởi động
Class cần chạy logic khi workbench khởi động (không gắn với lệnh nào) dùng registerWorkbenchContribution2:
// trích src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts
registerWorkbenchContribution2('EditTelemetryContribution', EditTelemetryContribution, WorkbenchPhase.AfterRestored);Các giai đoạn (src/vs/workbench/common/contributions.ts):
WorkbenchPhase | Khi nào | Ghi chú |
|---|---|---|
BlockStartup | Sắp khởi động | Chặn editor hiển thị → hạn chế dùng |
BlockRestore | Service sẵn sàng, sắp khôi phục UI | Cũng chặn hiển thị |
AfterRestored | View/panel/editor đã khôi phục | Phổ biến |
Eventually | 2–5 giây sau khi khôi phục | Cho việc không gấp |
Ngoài ra có kiểu lazy (chỉ tạo khi được yêu cầu) và kiểu theo editor (tạo khi một loại editor được mở). Comment trong code khuyến nghị ưu tiên Lazy.
7. Bài tập
- Mở
src/vs/workbench/contrib/emmet/browser/emmet.contribution.ts(chỉ 7 dòng) – nó import những gì? - Viết (trên giấy) một contribution giả định “SQL Formatter” gồm: 1 setting
sqlFormatter.keywordCase(upper/lower), 1 commandsqlFormatter.formathiện trong menu chuột phải editor khieditorLangId == sql.
Liên kết
- Phần lớn hàm đăng ký (
registerCommand,appendMenuItem,registerAction2…) trả vềIDisposableđể gỡ bỏ: Disposable & Event