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)

  1. Không có phụ thuộc từ bên ngoài contrib/ vào bên trong contrib/.
  2. Mỗi tính năng có một file .contribution.ts là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)).
  3. 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.
  4. 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.
  5. Service chỉ dùng trong một contrib → đăng ký ngay trong file .contribution.ts củ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 conRegistry.as<T>(id) – src/vs/platform/registry/common/platform.ts
Configuration registrySetting (cài đặt) hiển thị trong SettingsregisterConfiguration({...})
Views registryView (khung hiển thị) trong sidebar/panelregisterViews([...], container)
CommandsRegistryCommand (lệnh) theo IDregisterCommand(id, handler)
MenuRegistryMục menu (menu chuột phải, thanh tiêu đề view…)appendMenuItem(MenuId.X, {...})
registerAction2Gộp command + menu + keybinding (phím tắt) trong 1 classregisterAction2(class extends Action2 {...})
Workbench contributionsClass tự chạy ở một giai đoạn khởi độngregisterWorkbenchContribution2(id, ctor, phase)
Icon registryIcon có thể tùy biến theo themeregisterIcon(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 view

4.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);

SyncDescriptor

Là “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 IInstantiationService tạo TimelinePane và 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 đề when

Context key (khóa ngữ cảnh) là biến trạng thái toàn cục (ví dụ editorTextFocus, resourceExtname). Mệnh đề when là 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 trong package.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):

WorkbenchPhaseKhi nàoGhi chú
BlockStartupSắp khởi độngChặn editor hiển thị → hạn chế dùng
BlockRestoreService sẵn sàng, sắp khôi phục UICũng chặn hiển thị
AfterRestoredView/panel/editor đã khôi phụcPhổ biến
Eventually2–5 giây sau khi khôi phụcCho 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

  1. Mở src/vs/workbench/contrib/emmet/browser/emmet.contribution.ts (chỉ 7 dòng) – nó import những gì?
  2. Viết (trên giấy) một contribution giả định “SQL Formatter” gồm: 1 setting sqlFormatter.keywordCase (upper/lower), 1 command sqlFormatter.format hiện trong menu chuột phải editor khi editorLangId == sql.

Liên kết

  • Phần lớn hàm đăng ký (registerCommand, appendMenuItem, registerAction2…) trả về IDisposable để gỡ bỏ: Disposable & Event