Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

概要

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

やったことのあらまし

  • VSCodeの拡張機能を作る(当然)
  • doxyapp(前回参照)を拡張機能から実行して目的の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自身の機能を使いました。

外部プログラムの実行(コマンドラインでの実行)

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

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

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

ファイルの検索

これもnodejsのAPIであるfsを使えば簡単でした。

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を表示させるようにするには下記のようにするわけです。

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文字列の中に挿入する必要があります。

<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のプログラムにも下記のようなものがあります。

      // 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);
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で挿入することにしました。

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

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

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

おまけ: window Namespaceに関して

windowの基本的な使い方

これが持つメソッドを使うことで、エディターの操作や内容を取得したり、WebviewPanelのようなパネルを開いたりできます。

いま編集しているファイルの名前を得る

    let editor = vscode.window.activeTextEditor;
    if (!editor) { // これがないと警告される
        return "";
    }
    let working_file_path = editor.document.fileName;

編集しているファイルが変わったらイベントを受け取る

    vscode.window.onDidChangeVisibleTextEditors(
        () => {
            console.log("fire");
        }
    );

WebviewPanelを開く

何度か出してますがこれ:

    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の中身を更新する

            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が閉じられたイベントを取得する

そういうわけでこれも重要でした。

    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の拡張機能の事始めの記事は結構ありますが、それ以上の内容はあまりまとまっていない気がするので参考になれば幸いです。

hakua-doublemoon
横浜の外周部で組み込み装置のファームウェア開発やってます。RTOSのカーネル・ネットワーク関連からはじまりゴリゴリのHW依存のアプリケーション作ったりQtでGUI作ったり、LabVIEWでの画像処理とかもしてます。Ruby/RailsやRustが好きですがそれは本職とはあんまり関係ない。
https://pawoo.net/web/accounts/586636
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした