Monaco と Workbench:テキストバッファから IDE シェルへ
前提知識
- ›第1回:アーキテクチャとレイヤー構造
- ›第2回:起動フローとプロセスアーキテクチャ
- ›第3回:DI エンジンとサービスパターン
- ›第4回:Extension Host と API サーフェス
Monaco と Workbench:テキストバッファから IDE シェルへ
これまでの連載では、VS Code のエントリーポイントからプロセス生成、依存性注入、そして拡張機能ホストまでを順に追ってきました。今回はいよいよ、ユーザーが実際に目にする部分 ―― エディターと、それを取り囲む IDE シェル ―― を見ていきます。ここには2つの独立したシステムが存在します。スタンドアロンで組み込み可能なテキストエディターである Monaco エディターと、フル機能の IDE フレームである Workbench です。両者は明確な境界を介して組み合わさっています。どこまでが Monaco でどこからが Workbench なのかを理解することが、UI コードを読み解く上での鍵となります。
Monaco エディター:スタンドアロンアーキテクチャ
src/vs/editor/ 以下はすべて Monaco エディターです。npm で公開されているスタンドアロンの Monaco Editor や、microsoft.github.io/monaco-editor のプレイグラウンドを動かしているのも、まさにこのエンジンです。Monaco は base/ と platform/ にのみ依存し、workbench/ には一切依存しません。この独立性は、第1回で解説したレイヤールールによって強制されています。
エディターは独自の内部アーキテクチャを持っています。
graph BT
subgraph "src/vs/editor/"
MODEL["<b>common/model/</b><br/>Piece table text buffer,<br/>line tokenization"]
VM["<b>common/viewModel/</b><br/>Cursor state, decorations,<br/>scroll position"]
VIEW["<b>browser/view/</b><br/>Rendering pipeline,<br/>GPU-accelerated canvas"]
CONTRIB["<b>contrib/</b><br/>40+ features:<br/>find, fold, suggest, hover..."]
end
MODEL --> VM
VM --> VIEW
MODEL --> CONTRIB
VM --> CONTRIB
VIEW --> CONTRIB
style MODEL fill:#e8f5e9
style VM fill:#e3f2fd
style VIEW fill:#fff3e0
style CONTRIB fill:#fce4ec
model レイヤー(common/model/)は、ピーステーブルというデータ構造を使ってテキストバッファを実装しています。追記専用のアプローチにより、ドキュメントのサイズに関わらず挿入・削除が O(log n) で行えます。また、トークン化、バッファレベルでのブラケットマッチング、テキスト検索も担当します。
viewModel レイヤー(common/viewModel/)は、model と view の間に位置します。カーソル位置、選択範囲、デコレーション(ハイライト、エラーの波線、git ガター)を管理し、モデル上の行番号と画面上の行番号を対応付けます(ワードラップやコード折りたたみが有効な場合、両者は異なります)。
view レイヤー(browser/view/)はレンダリングを担当します。VS Code のエディターは、DOM ベースのレンダリングと GPU アクセラレーションを使ったキャンバスレンダリング(editor.experimentalGpuAcceleration で制御)を使い分けるという洗練されたアプローチを採用しています。view は複数の view part に分割されており、行番号、ミニマップ、コンテンツエリア、スクロールバー、オーバーレイウィジェットなど、それぞれが独立して更新可能です。
エディター Contribution とインスタンス化モード
Monaco の拡張ポイントシステムは、Workbench の contribution システムとは独立しており、それよりも以前から存在しています。エディター機能はエディター contributionとして自身を登録し、そのタイミングは EditorContributionInstantiation enum で制御されます。
export const enum EditorContributionInstantiation {
Eager, // Created when the editor is instantiated
AfterFirstRender, // Within 50ms after first text render
BeforeFirstInteraction, // Before first mouse/keyboard event
Eventually, // At idle time, within 5000ms
Lazy, // Only when explicitly requested
}
flowchart LR
EAGER["<b>Eager</b><br/>View state save/restore<br/>Cursor blinking"] --> AFR["<b>AfterFirstRender</b><br/>Syntax highlighting<br/>Bracket matching"]
AFR --> BFI["<b>BeforeFirstInteraction</b><br/>Find widget<br/>Autocomplete"]
BFI --> EVT["<b>Eventually</b><br/>Code lens<br/>Folding ranges"]
EVT --> LAZY["<b>Lazy</b><br/>On-demand only"]
この5段階のインスタンス化システムは、第3回で紹介した Workbench の4フェーズ構成(WorkbenchPhase)よりも細かく設計されています。AfterFirstRender と BeforeFirstInteraction というティアは、エディター固有の関心事である入力レイテンシに対応したものです。たとえば検索ウィジェットは、ユーザーが Ctrl+F を押す前に必ず準備が完了していなければなりませんが、初期レンダリング時には存在している必要はありません。
editor.all.ts バレルファイルは、約40個のエディター contribution をすべてインポートします。import の数を数えるだけで、Monaco の機能の全体像がつかめます。anchor select、ブラケットマッチング、クリップボード、コードアクション、コードレンズ、カラーピッカー、コメント、コンテキストメニュー、カーソルアンドゥ、ドラッグ&ドロップ、検索、折りたたみ、フォーマット、シンボルへ移動、ホバー、インデント、インライン補完、リンク、マルチカーソル、パラメーターヒント、リネーム、セマンティックトークン、スニペット、スティッキースクロール、サジェスト、そしてその他多数です。
ヒント:
src/vs/editor/contrib/以下の各エディター contribution は、それ自体で完結したモジュールになっています。特定のエディター機能(例:コード折りたたみ)の仕組みを理解したいときは、src/vs/editor/contrib/folding/browser/folding.tsを起点にしましょう。ファイルの末尾に contribution の登録があり、その上にすべての機能ロジックが記述されています。
Workbench シェル:レイアウトとパーツ
Monaco がテキストエディターであるのに対し、Workbench クラスは IDE シェルです。Layout を継承しており、シリアライズ可能なグリッド上に配置されたパーツを管理します。
graph TD
subgraph "Workbench Layout"
TITLE["Titlebar Part"]
AB["Activity Bar"]
SB["Sidebar Part"]
EA["Editor Area<br/><i>(contains Monaco instances)</i>"]
PANEL["Panel Part"]
AUX["Auxiliary Bar Part"]
STATUS["Status Bar Part"]
end
TITLE --- AB
AB --- SB
SB --- EA
EA --- PANEL
EA --- AUX
STATUS --- EA
style EA fill:#e8f5e9
style SB fill:#e3f2fd
style PANEL fill:#fff3e0
src/vs/workbench/browser/layout.ts の Layout クラスは、コードベース全体の中でも最も複雑なクラスのひとつです。シグネチャは export abstract class Layout extends Disposable implements IWorkbenchLayoutService であり、以下を管理しています。
- グリッドベースのシリアライズ ―― Workbench のレイアウトは
base/browser/ui/grid/のSerializableGridとして表現されます。パーツはリサイズや並べ替えが可能で、レイアウト状態全体がStorageServiceに永続化されます。 - パーツの表示制御 ―― サイドバー、パネル、補助バー、ステータスバーはそれぞれ切り替え可能です。
nosidebarやnopanelといった CSS クラスがルート要素に適用されます。 - Zen モード ―― エディター以外をすべて非表示にする特別なレイアウト状態です。
- マルチウィンドウサポート ―― パーツは
SINGLE_WINDOW_PARTS(タイトルバー、アクティビティバー)とMULTI_WINDOW_PARTS(エディターグループ、補助ウィンドウ)に分類されます。
workbench.ts#L131-L190 の Workbench.startup() メソッドは、初期化の全体を次の順序でオーケストレーションします。
- emitter のリーク閾値を 175 に設定 ―― VS Code の Workbench は多数のイベントリスナーを持つため、誤検知によるリーク警告を防ぎます。
- サービスの初期化 ―― すべての
registerSingleton()ディスクリプターをコンテナに収集します。 - レジストリの開始 ―― Workbench とエディターファクトリーの contribution レジストリを起動します。
- Workbench のレンダリング ―― すべてのパーツに対応する DOM 構造を生成します。
- Workbench レイアウトの作成 ―― グリッドシステムをセットアップします。
- レイアウト ―― 最初のレイアウトパスを実行します。
- 復元 ―― 前回のセッションからエディター、ビュー、パネルの状態を復元します。
デスクトップと Web:バレルファイルの違い
第1回で触れたとおり、何がロードされるかはバレルファイルで決まります。具体的な違いを見てみましょう。
graph TD
subgraph COMMON["workbench.common.main.ts"]
C1["Editor core (editor.all.ts)"]
C2["Workbench actions"]
C3["API extension points"]
C4["Editor parts"]
C5["150+ shared services"]
C6["All contrib/ features"]
end
subgraph DESKTOP["workbench.desktop.main.ts"]
D0["imports common"]
D1["Native file dialogs"]
D2["Native menus"]
D3["Desktop lifecycle"]
D4["Electron clipboard"]
D5["Native title service"]
D6["PTY-based terminal"]
D7["Local extension management"]
end
subgraph WEB["workbench.web.main.ts"]
W0["imports common"]
W1["Browser file dialogs"]
W2["Web lifecycle"]
W3["Browser clipboard"]
W4["Web extension scanning"]
W5["Browser search"]
W6["Web URL service"]
end
DESKTOP --> COMMON
WEB --> COMMON
style COMMON fill:#e8f5e9
style DESKTOP fill:#e3f2fd
style WEB fill:#fff3e0
workbench.desktop.main.ts は約50個のデスクトップ固有モジュールをインポートします。各モジュールは通常 registerSingleton() を呼び出し、サービスインターフェースを Electron 固有の実装にバインドします。たとえばネイティブのファイルダイアログサービスは、ブラウザの <input type="file"> を Electron の dialog.showOpenDialog() に置き換えます。
workbench.web.main.ts は約40個の Web 固有モジュールをインポートし、ブラウザ向けの代替実装を提供します。Web の検索サービスは ripgrep ではなくブラウザのネイティブなテキスト検索を使用します。Web のライフサイクルサービスは Electron の will-quit ではなく beforeunload を処理します。
この設計こそが vscode.dev を実現している核心です ―― 同じ contribution 機能、同じエディター、同じ拡張機能 API(WebWorker の制約の範囲内で)を持ちながら、サービス実装だけをブラウザ互換のものに差し替えているのです。
主要な Workbench 機能:Chat、ターミナル、SCM、デバッグ
あらゆる主要 IDE 機能は、src/vs/workbench/contrib/ 以下に自己完結した contribution として配置されています。その規模を見てみましょう。
| Contribution | パス | 登録内容 |
|---|---|---|
| ターミナル | contrib/terminal/ |
ビュー、コマンド、キーバインディング、リンクプロバイダー、シェル統合 |
| SCM(Git) | contrib/scm/ |
ソース管理ビューレット、変更デコレーション、ステータスバーアイテム |
| デバッグ | contrib/debug/ |
デバッグビューレット、ブレークポイントデコレーション、コールスタック、REPL |
| Chat/AI | contrib/chat/ |
チャットパネル、インラインチャット、エージェント、言語モデル統合 |
| 検索 | contrib/search/ |
検索ビューレット、ファイル横断置換、検索エディター |
| 拡張機能 | contrib/extensions/ |
拡張機能ビューレット、マーケットプレイス、推薦機能 |
| Notebook | contrib/notebook/ |
Notebook エディター、セルレンダリング、カーネル管理 |
各 contribution は、第3回で確立したのと同じパターンに従っています。
- contribution 固有のサービスがあれば、
registerSingleton()でサービスを登録する。 registerWorkbenchContribution2()で適切なWorkbenchPhaseを指定して contribution を登録する。CommandsRegistryまたはAction2でコマンドを登録する。ViewsRegistryでサイドバー・パネルのビューを登録する。KeybindingsRegistryでキーバインディングを登録する。MenuRegistryでコンテキストメニューやコマンドパレット用のメニューを登録する。
flowchart TD
subgraph "contrib/terminal/"
T1["terminal.contribution.ts<br/><i>Entry point: registers everything</i>"]
T2["terminalService.ts<br/><i>Core service: manages instances</i>"]
T3["terminalView.ts<br/><i>Panel view</i>"]
T4["terminalActions.ts<br/><i>Commands & keybindings</i>"]
T5["links/<br/><i>URL detection & handling</i>"]
end
T1 --> T2
T1 --> T3
T1 --> T4
T1 --> T5
ターミナル contribution は、このアーキテクチャがどう機能するかを示す好例です。ITerminalService シングルトン(ターミナルインスタンスを管理)、ターミナルパネルビュー(インスタンスを描画)、多数のコマンド(新規ターミナル、分割、終了、クリア)、キーバインディング(Ctrl+`)、URL 検出のためのリンクプロバイダーをすべて登録しています。これらはすべて workbench.common.main.ts を通じてインポートされるため、デスクトップと Web の両方で利用可能になります。
Chat/AI contribution(contrib/chat/)は最も新しく追加された主要機能です。同じパターンに従いながら、追加の概念も導入しています。チャットエージェント(第4回で見た拡張機能 API を通じて登録)、言語モデルツール、そしてインラインチャット(コードエディター内にチャットインターフェースを埋め込むエディターゾーンウィジェット)です。既存のアーキテクチャに根本的な変更を加えることなく、新機能がどう組み合わさるかを示す好事例と言えます。
ヒント: 任意の IDE 機能を理解するには、
src/vs/workbench/contrib/以下にある*.contribution.tsファイルを探しましょう。サービス、ビュー、コマンド、キーバインディングのすべてが登録されるエントリーポイントは常にここです。まずこのファイルを起点に、import を辿っていきましょう。
全体像
この5回の連載を通じて、VS Code の最も外側のシェルから内部の実装まで、一通り追ってきました。
-
アーキテクチャとレイヤー構造 ―― 4つの柱(
base、platform、editor、workbench)と環境レイヤー(common、browser、node、electron-*)により、単一のコードベースでデスクトップ、Web、リモートのすべてに対応する仕組み。 -
起動フローとプロセス ―― main、renderer、shared、utility、extension host プロセスにまたがるマルチプロセス起動シーケンスと、できる限り早く画面にピクセルを表示するための設計。
-
DI とパターン ――
createDecorator/InstantiationServiceによる独自の依存性注入システム、起動パフォーマンスのためのレイジープロキシ、そしてすべてのサービスの基盤となる基本パターン(Disposable、Event/Emitter、Registry)。 -
Extension Host ―― 3種類のホスト、140以上の RPC プロキシインターフェース、そして
vscode.*API 名前空間のランタイム構築を備えた、独立した拡張機能ランタイム。 -
Monaco と Workbench ―― 独自の contribution システムを持つスタンドアロンエディターエンジンと、グリッドベースのレイアウトと数百の feature contribution でそれを包む IDE シェル。
一貫したテーマは、慣習とツールによって強制される関心の分離です。レイヤーシステムはコードが適切な場所で動くことを保証します。DI システムはサービスが正しく接続されることを保証します。contribution パターンは機能が適切なタイミングでロードされることを保証します。そしてバレルファイルは各プラットフォームに必要なコードだけが届くことを保証します。
ファイル数5,700を超える TypeScript コードベースでも、正しいアーキテクチャがあれば読み解けるということを、VS Code は証明しています。これで地図は手に入りました。