📝

【VS Code】巨大なMermaid図が見づらい!を解決するズーム&パン機能付き拡張機能を作って公開した話

に公開

はじめに

VS Code Marketplace に「mermaid-pan-zoom」を公開しました!

この拡張は、Markdown に書いた Mermaid 記法の図を、拡大・縮小・パン操作しながらプレビューできる VS Code 拡張機能です。

この記事では、開発の過程でやったこと、つまずいたポイントとその解決策をまとめました。
VS Code の拡張機能を自作してみたい方や、Mermaid × React × WebView に興味がある方の参考になれば嬉しいです!

開発の動機

弊社開発チームではドキュメント管理に Markdown をよく使っています。
GitHub では Mermaid を使ってシーケンス図やフローチャート、ER 図などを描画できてとても便利なのですが、ER 図のようにテーブル数が 100 個近くあるケースだと、1 つ 1 つのテーブルが小さくなりすぎて見づらいという問題がありました。

GitHub のビューアーにも拡大機能はあるものの、正直あまり使いやすくありません…。

VS Code の拡張機能も探してみましたが、「Mermaid をプレビューできるもの」はあっても、パン(移動)やズームができるものが見当たらなかった(見落としていただけかもしれません)ので、自分で作ってみることにしました。

もともと VS Code の拡張を自作して公開してみたいと思っていたので、楽しみながら作れました!

開発の流れ

1. 拡張機能の雛形を作成

まずは公式ドキュメントのチュートリアルを参考に、雛形を作成しました。

  • yo code で VS Code 拡張の基本プロジェクトを生成
  • UI 表示に webview を利用するテンプレートを選択
  • エントリーポイントは extension.ts、WebView 側の描画は panel.ts に分離

2. WebView 側で React を使えるようにする

WebView 内の UI は React + TypeScript で構築しました。

npm create vite@latest
npm i react react-dom mermaid react-svg-pan-zoom
npm i -D esbuild typescript eslint prettier

正直、今回のケースだけでいえば React を使わず Vanilla JS で書いた方が早く終わったと思います(1 日かからなかったかも…)。

ただ、今後もっとインタラクティブな UI を持つ拡張機能を作りたいときに React が使えると強いので、今回は勉強も兼ねて導入しました。

3. Mermaid × Pan/Zoom の組み合わせ

  • Mermaid: mermaid.render() で SVG を生成
  • Pan/Zoom: react-svg-pan-zoom を使って操作可能に

やっていることは単純で、Mermaid で SVG を作って、react-svg-pan-zoom で操作できるようにするだけです。

Mermaid 記法から SVG を生成する処理はカスタムフックに切り出していて、VS Code との通信(メッセージ送受信)は公式の WebView APIを参考に実装しました。

// webview-ui/src/hooks/useMermaidText.ts

import { useEffect, useState } from "react";
import { vscode } from "../utils";

export function useMermaidText(): string {
  const [mermaidText, setMermaidText] = useState<string>("");

  const handleMessage = (event: MessageEvent) => {
    const message = event.data;
    if (message.type === "send-mermaid") {
      setMermaidText(message.payload);
    }
  };

  useEffect(() => {
    vscode.postMessage({ type: "get-mermaid" }); // 初期メッセージを送信して取得
    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, []);

  return mermaidText;
}

つまずいたポイントと解決策

Mermaid の ID 問題

Mermaid が内部で生成する SVG の id数字で始まると React 側でエラーになる問題がありました。

解決策: 正規表現で置換し、必ず mermaid- プレフィックスを付けるようにしました。

const replacedSvg = svg.replace(new RegExp(uuid, "g"), `mermaid-${uuid}`);

ReactSVGPanZoom が更新されない

valuenull のままだとビューが動かず、ズームなどが反応しません。
解決策: onChangeValue で状態を管理するようにしました。

<ReactSVGPanZoom
  value={value}
  onChangeValue={setValue}
>

WebView で画像が読み込めない

public 配下のファイルは読めるのに、assets 内の画像が参照できない問題がありました。

解決策:

  • import img from "../public/xxx.svg"; のようにしてバンドル対象に含める
  • もしくは webview.asWebviewUri を使って安全な URI に変換する

CSP (Content Security Policy) 対応

WebView ではセキュリティ上、スクリプトに nonce を付与する必要があります。

解決策:
index.html を読み込んだあとで nonce を発行・付与する処理を入れて対応しました。


処理の流れ(ざっくり)

1. Mermaid 記法から SVG 文字列を生成

// webview-ui/src/components/organizations/Mermaid.tsx
const initialize = async (id: string, mermaidText: string) => {
  const svg = await generateMermaidSvg({ id, mermaidText, mermaid });
  if (!svg) return console.error("Failed to generate Mermaid SVG");

  const matched = svg.match(/<svg[^>]*?id="(.+?)"/);
  if (!matched) return console.error("Failed to find SVG ID");

  const uuid = matched[1];
  const replacedSvg = svg.replace(new RegExp(uuid, "g"), `mermaid-${uuid}`);
  setSvgString(replacedSvg);
};

2. SVG を表示し、パン・ズーム機能を適用

return (
  <>
    {svgString && (
      <ReactSvgPanZoomLoader
        svgXML={svgString}
        render={(content) => (
          <ReactSVGPanZoom
            className="mermaid"
            ref={panZoomRef}
            width={windowSize.width}
            height={windowSize.height}
            value={value}
            onChangeValue={setValue}
            tool={currentTool}
            background="#fff"
            modifierKeys={["Alt", "Shift", "Control"]}
            onChangeTool={setCurrentTool}
            customMiniature={() => <></>}
            customToolbar={() => (
              <Toolbar
                tool={currentTool}
                onChangeTool={setCurrentTool}
                value={value}
                onChangeValue={setValue}
              />
            )}
          >
            <svg>{content}</svg>
          </ReactSVGPanZoom>
        )}
      />
    )}
  </>
);

Marketplace 公開までの流れ

1. package.json の設定

最低限の情報を記載します。

{
  "name": "mermaid-pan-zoom",
  "displayName": "mermaid-pan-zoom",
  "description": "Zoom & Pan enabled Mermaid preview for VS Code",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.102.0"
  },
  "categories": ["Other"],
  "activationEvents": ["onLanguage:markdown"],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "mermaidPanZoom.open",
        "title": "mermaidPanZoom プレビューを開く"
      }
    ]
  }
}

2. 公開コマンド

npm install -g @vscode/vsce
vsce package
vsce publish

使い方

  1. Markdown に Mermaid のコードブロックを書く
  2. コマンドパレットで mermaidPanZoom プレビューを開く を実行
  3. ズームイン・ズームアウト・ドラッグで快適に操作!

まとめ

  • React × Mermaid × WebView を使って VS Code 拡張を構築
  • ID 問題・画像読み込み・CSP 対応などでハマった
  • 最終的に Marketplace に公開できた 🎉
  • React を使った WebView の作り方がわかったので、次はもっとリッチな拡張機能にも挑戦してみたい

拡張はこちらからインストールできます 👇
👉 mermaid-pan-zoom

次回は、もう少し具体的なサンプルを交えて「VS Code 拡張の作り方」を掘り下げて紹介する予定です。

参考リンク

H&Companyテックブログ

Discussion