Read OSS

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)よりも細かく設計されています。AfterFirstRenderBeforeFirstInteraction というティアは、エディター固有の関心事である入力レイテンシに対応したものです。たとえば検索ウィジェットは、ユーザーが 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.tsLayout クラスは、コードベース全体の中でも最も複雑なクラスのひとつです。シグネチャは export abstract class Layout extends Disposable implements IWorkbenchLayoutService であり、以下を管理しています。

  • グリッドベースのシリアライズ ―― Workbench のレイアウトは base/browser/ui/grid/SerializableGrid として表現されます。パーツはリサイズや並べ替えが可能で、レイアウト状態全体が StorageService に永続化されます。
  • パーツの表示制御 ―― サイドバー、パネル、補助バー、ステータスバーはそれぞれ切り替え可能です。nosidebarnopanel といった CSS クラスがルート要素に適用されます。
  • Zen モード ―― エディター以外をすべて非表示にする特別なレイアウト状態です。
  • マルチウィンドウサポート ―― パーツは SINGLE_WINDOW_PARTS(タイトルバー、アクティビティバー)と MULTI_WINDOW_PARTS(エディターグループ、補助ウィンドウ)に分類されます。

workbench.ts#L131-L190Workbench.startup() メソッドは、初期化の全体を次の順序でオーケストレーションします。

  1. emitter のリーク閾値を 175 に設定 ―― VS Code の Workbench は多数のイベントリスナーを持つため、誤検知によるリーク警告を防ぎます。
  2. サービスの初期化 ―― すべての registerSingleton() ディスクリプターをコンテナに収集します。
  3. レジストリの開始 ―― Workbench とエディターファクトリーの contribution レジストリを起動します。
  4. Workbench のレンダリング ―― すべてのパーツに対応する DOM 構造を生成します。
  5. Workbench レイアウトの作成 ―― グリッドシステムをセットアップします。
  6. レイアウト ―― 最初のレイアウトパスを実行します。
  7. 復元 ―― 前回のセッションからエディター、ビュー、パネルの状態を復元します。

デスクトップと 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回で確立したのと同じパターンに従っています。

  1. contribution 固有のサービスがあれば、registerSingleton()サービスを登録する。
  2. registerWorkbenchContribution2() で適切な WorkbenchPhase を指定して contribution を登録する。
  3. CommandsRegistry または Action2コマンドを登録する。
  4. ViewsRegistry でサイドバー・パネルのビューを登録する。
  5. KeybindingsRegistryキーバインディングを登録する。
  6. 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 の最も外側のシェルから内部の実装まで、一通り追ってきました。

  1. アーキテクチャとレイヤー構造 ―― 4つの柱(baseplatformeditorworkbench)と環境レイヤー(commonbrowsernodeelectron-*)により、単一のコードベースでデスクトップ、Web、リモートのすべてに対応する仕組み。

  2. 起動フローとプロセス ―― main、renderer、shared、utility、extension host プロセスにまたがるマルチプロセス起動シーケンスと、できる限り早く画面にピクセルを表示するための設計。

  3. DI とパターン ―― createDecorator/InstantiationService による独自の依存性注入システム、起動パフォーマンスのためのレイジープロキシ、そしてすべてのサービスの基盤となる基本パターン(Disposable、Event/Emitter、Registry)。

  4. Extension Host ―― 3種類のホスト、140以上の RPC プロキシインターフェース、そして vscode.* API 名前空間のランタイム構築を備えた、独立した拡張機能ランタイム。

  5. Monaco と Workbench ―― 独自の contribution システムを持つスタンドアロンエディターエンジンと、グリッドベースのレイアウトと数百の feature contribution でそれを包む IDE シェル。

一貫したテーマは、慣習とツールによって強制される関心の分離です。レイヤーシステムはコードが適切な場所で動くことを保証します。DI システムはサービスが正しく接続されることを保証します。contribution パターンは機能が適切なタイミングでロードされることを保証します。そしてバレルファイルは各プラットフォームに必要なコードだけが届くことを保証します。

ファイル数5,700を超える TypeScript コードベースでも、正しいアーキテクチャがあれば読み解けるということを、VS Code は証明しています。これで地図は手に入りました。