VSCodeでHTMLを表示する拡張機能を作る

# 概要
この記事は[DoxygenPreviewer](https://qiita.com/hakua-doublemoon/items/c328a7bf0bc7a1fbef14)のMaking第三弾です。（これで終わりです）
前回までDoxygenをどうにかこうにかしてきたので、それをVisualStudioCodeと連携させて使いやすくします。
拡張機能の作成はTypeScriptが基本っぽいのでTypeScriptを使いました。
ちなみに筆者は今はじめてYeomanのことを知りました。（nodejsはかろうじて使ったこともあるが）
ソースコードは[github](https://github.com/hakua-doublemoon/DoxygenPreviewer)にあります。

## やったことのあらまし

* VSCodeの拡張機能を作る（当然）
* doxyapp（[前回参照](https://qiita.com/hakua-doublemoon/items/150a485770d07262eb6c)）を拡張機能から実行して目的のHTMLを得る
* HTMLをVSCodeに表示する

# 詳細
## VSCodeの拡張機能の開発を始める
これ自体は下記などいろいろな人が書いてます。
https://qiita.com/tiibun/items/31d613af4352e1a9c8af
なので項目だけ挙げますと:

1. nodejsをインストールする
2. "install Yeoman and VS Code Extension Generator with:"

    > $ npm install -g yo generator-code

3. Run the generator and fill out a few fields:

    > $ yo code
    なんか聞かれるので適当に答えます。

4. VSCodeが起動してextension.tsとか開かれるので適当に中身を書く
5. F5でデバッグ実行できます
6. vsceのnpmをインストールして`vsce package`するとvsixが作成されますのでこれを配布することもできます

## VSCodeの拡張機能コードから外部プログラムを実行したりする

VSCodeのAPIでもこれができるかもしれませんが、TypeScript自身の機能を使いました。

### 外部プログラムの実行（コマンドラインでの実行）

```ts:doxygen_exec_main()@extension.ts
    const doxyapp_path = path.join(current_path, "bin", "doxyapp.exe");
    child_process.execSync(doxyapp_path + " " + dst_path);
```

[child_process](https://nodejs.org/api/child_process.html)はNode.jsのAPIみたいですね。`${current_path}/bin/doxyapp.exe ${file_name}`というような感じでフルパスで実行ファイルと処理するファイルを指定しています。
なお実行ファイルは拡張機能とセットにしてパッケージ化できます。パッケージが展開されたパスは`context`から`context.extensionPath`という感じで得られます。
一方で環境変数を得ることもできますので、Tempパスとかを使うこともできます。

```ts:extension.ts
const env = process.env; // 環境変数からtmpフォルダを求める
const output_dir = (env.Tmp ? path.join(env.Tmp, "DoxygenViewer") : "");
```

### ファイルの検索

これもnodejsのAPIである[fs](https://nodejs.org/api/fs.html)を使えば簡単でした。

```ts:doxygen_exec_main()@extension.ts
    const file_list = fs.readdirSync(html_dir);
:
    file_list.forEach(
        function(fn) {
            if (fn.includes(src_file_name) && !fn.includes("_source")) {
```

readdirで指定したディレクトリのファイル一覧を得て、forEachで探索しながらincludesで目的のファイル名を得ます。
fsやJavaScriptの模範的なコーディングとしてはreaddirSyncではなくreaddirを使って時間のかかる処理を非同期で実行することだと思いますが、今回の範囲だと動機実行してもさほどのレイテンシにならないのでやってません。
ちなみに何をしているかといえば、doxyappで生成したhtmlの中からソースファイルの名前を含み、かつ`_source`という文字列をファイル名に持たないhtmlファイルを探しています。

## HTMLを表示する

ここでVSCodeのAPIをうまく使う必要があります。
VSCodeにはWebviewAPIと呼ばれるものがあり、下記のようにガイドも用意されています。
https://code.visualstudio.com/api/extension-guides/webview
例えばhtmlを表示させるようにするには下記のようにするわけです。

```ts:activate()@extension.ts
    let panel_obj = vscode.window.createWebviewPanel(
        // 詳細はコードを見てください
    );
    panel_obj.webview.html
        = html_string_get(doc, cssSrc); // なんかhtml文字列を返します。
```

こいつの難しいところはCSSを読み込ませることです。デフォルトの状態では、VSCodeのセキュリティでCSSは読み込まれないようにされていて……結論を言うと下記のような文字列を```webview.html```にいれるhtml文字列の中に挿入する必要があります。

```html
<link href="vscode-resource:/c:/Temp/DoxygenViewer/html/doxygen.css" rel="stylesheet" type="text/css">
```

このことは上記のガイドにもこう書いてあったりします。(Google翻訳)

> Webviews run in isolated contexts that cannot directly access local resources. This is done for security reasons. This means that in order to load images, stylesheets, and other resources from your extension, or to load any content from the user's current workspace, you must use the vscode-resource: scheme inside the webview.
(Webビューは、ローカルリソースに直接アクセスできない独立したコンテキストで実行されます。 これはセキュリティ上の理由から行われています。 つまり、拡張機能から画像、スタイルシート、その他のリソースをロードしたり、ユーザーの現在のワークスペースからコンテンツをロードしたりするには、Webビュー内でvscode-resource：スキーマを使用する必要があります。)

で、ガイドにある猫がキーボードを乱打するCatCodingのプログラムにも下記のようなものがあります。

```ts
      // Get path to resource on disk
      const onDiskPath = vscode.Uri.file(
        path.join(context.extensionPath, 'media', 'cat.gif')
      );

      // And get the special URI to use with the webview
      const catGifSrc = onDiskPath.with({ scheme: 'vscode-resource' });

      panel.webview.html = getWebviewContent(catGifSrc);
```
```ts
function getWebviewContent(cat: keyof typeof cats) {
  return `<!DOCTYPE html>
:
<body>
    <img src="${cats[cat]}" width="300" />
</body>
</html>`;
}
```
しかしこれだけだと結局何なのかわかりません。まず、srcに何か文字列を指定してやってることはわかりますが、具体的にどういう文字列になるのかはconsole.log()とか何らかの方法で文字列変数の中身を確認する必要があります。
DoxygenPreviewerの話に戻ると上に出したような文字列がそれに当たるのでした。

```
vscode-resource:/c:/Temp/DoxygenViewer/html/doxygen.css
```

そしてもう一つの問題は、catCodingでは決まったHTML文字列に式展開していましたが、今やりたいのはhtmlファイルを読んだ中身に`vscode-resource`を反映することです。これは最終的に、下記のようにしてファイルから得た文字列に、`vscode-resource`を反映すればこうなるだろうcssへのhrefタグを別途作ってspliceで挿入することにしました。

```ts:html_string_get()@extension.ts
    line_strs.splice(15, 0, css_link_tag); // 15行目にcssの指定があるのでそれに合わせる。
```

あと下記のようにlocalResourceRootsの設定も必要みたいです。

```ts
    let panel_obj = vscode.window.createWebviewPanel(
:
        vscode.ViewColumn.Beside, 
        {
            localResourceRoots: resource_root,
        }
    );
```

# おまけ: window Namespaceに関して
## windowの基本的な使い方
これが持つメソッドを使うことで、エディターの操作や内容を取得したり、WebviewPanelのようなパネルを開いたりできます。

### いま編集しているファイルの名前を得る
```ts
    let editor = vscode.window.activeTextEditor;
    if (!editor) { // これがないと警告される
        return "";
    }
    let working_file_path = editor.document.fileName;
```

### 編集しているファイルが変わったらイベントを受け取る
```ts
    vscode.window.onDidChangeVisibleTextEditors(
        () => {
            console.log("fire");
        }
    );
```

### WebviewPanelを開く
何度か出してますがこれ:

```ts
    let panel_obj = vscode.window.createWebviewPanel(
        "doxy", 
        "doxygen browser", 
        vscode.ViewColumn.Beside, 
        {
            enableScripts: true,
            enableFindWidget: true,
            localResourceRoots: resource_root,
        }
    );
```

第一と第二引数はなんでもいいですが、第三引数と第四引数のオブジェクトは重要です。
`vscode.ViewColumn.Beside`をするとカラムを増やす形で新しいタブを開けます。catCodingみたいにカラムを増やさないなら`vscode.ViewColumn.One`とかにできます。ほかにもいろいろありますがよくわかりません。
第四引数は……とりあえず`localResourceRoots: resource_root`は忘れないようにしましょう。あとは今回は特に役に立ってない気がします（でも書いてある）。



### WebviewPanelの中身を更新する

```ts
            panel_obj.webview.html = html_string_get(doc, cssSrc);
```
このようにpanelのオブジェクトのwebview.htmlプロパティを設定しなおせば中身が更新されます。
なお、VSCodeの拡張機能コードの仕様？として、初めて拡張機能が実行されるとactivate()から実行されますが、次回以降はregisterCommand()の中身のみ実行されます。そしてactivate()が実行されるとactivate()直下およびグローバルの変数の中身は維持されるようです。
そのためDoxygenPreviewerではactivate()時に`panel_obj`を宣言し、以降ではpanelが開いていればhtmlのみ更新し、一度閉じられれば再度panelを開くように動作しています。

### WebviewPanelが閉じられたイベントを取得する
そういうわけでこれも重要でした。

```ts
    panel_obj.onDidDispose( () => {
        console.log("onDidDispose");
        panel_alive = false;
    });
```

### WebviewPanelがスクロールされたイベントを取得する
**できてません。**
そのためDoxygenPreviewerは中身を更新するたびにhtmlが先頭に戻ってしまいます。
基本的にtexteditorの方ではスクロールのイベントを受け取れますが、WebviewPanelはそれに対応していないようです。
しかしplantumlではpreviewを動かさずに内容を更新しているので何か方法があるのかと思います。
またScriptを埋め込んで動かすことはできるので、直接通信はできないので、ループバックとかで強引に通信することを考えていたりします。何か良い案があったら教えてください。

### WebviewPanelのHTMLを開発者ツールで見る
VSCodeのコマンドで`Developer: Open Webview Developer Tools`と打つと開発者ツールを開けます。iframeになってたりしてあれですが、適当にポチポチしてると中身が見れたりします。

# 最後に: VSCodeのAPIリファレンスは良くないです
VSCodeのAPIはvscodeを先頭に、commands, comments, ..., windows, ...というようなnamespaceがあり、その下にそれぞれのメソッドやプロパティなどがあります。
すべてまとめるとそこそこの量になりますが、VSCodeのリファレンスはこれを一つのページにしています。そしてメソッド、プロパティレベルになると閉じた状態で表示されるので内容で検索できません。それとnamespace内のclassは目次に書いてないので見通しが悪いです。
Qtとかはこの辺の構造がしっかりしててサンプルコードも豊富なので読みやすいのですが……。

と、思ったよりたくさん書いてしまいました。VSCodeの拡張機能の事始めの記事は結構ありますが、それ以上の内容はあまりまとまっていない気がするので参考になれば幸いです。