Node.js
TypeScript
VSCode
VisualStudioCode

VSCode拡張機能開発で次の一歩 -- ファイル操作 & クリップボード操作 & 外部アプリ操作 & 自作拡張機能紹介 merge-n-paste

Windows環境を元に書いています。Mac,Linuxでも変わりないと思いますが、予めご了承ください。
あと、多少読み物気取りになってるので、適当に読み飛ばしてください。

はじめに

VSCodeいいですよね。なんといっても動作が軽快。例えば、数百MBクラスの大きなファイルの読み込みはnotepad++やgVimにやや劣るものの、国産定番エディタ(Hidemaru,SakuraEditor)よりも高速に処理できるようです。(※Hidemaruは遅延処理+キャッシュ処理の様ですが、キャッシュ無で最後の行の読み込みが完了する時間を比較基準にしています)同じElectronベースのAtomは・・・不思議に思うぐらい重いんですよね・・まあ、エディタパフォーマンス比較は別の機会で行うとして・・

拡張機能開発次の一歩

私の経験ですが、マクロ代わりに自分用で公開しない拡張機能を作ろうと思い、HelloWorldを出すところまではQiitaさんやその他ドキュメントのお世話になって作るのですが、マクロとして使うには案外奥深く、それ以降は放置されていました。
単純に「忙しかった」って理由もあるのですが、そこから次の一歩を踏み出すのにちょうどいいユルさのドキュメント(特にVSCode絡みで何ができるか)がまとめられていたらスムーズに踏み出せたかな・・と思うところもありまして、今回書いてみたいと思います。

拡張機能開発「次の一歩」として以下のものは揃っている前提で話を進めます。揃っていない・これから揃える方も拡張機能で何ができるかの知識として参考にしていただければと思います。

どこまでやるか

「エディタマクロ」チックなことをする上で

  • エディタからテキストを取得
  • 取得したテキストをテンポラリファイルに書き出し
  • 外部アプリを実行
  • テンポラリファイルを読み込み
  • 読み込まれたテキストをエディタに反映

これで、「とりあえず手持ちのプリプロセッサやフィルタを使いまわす」事ができるはずです。(本来は内部処理で完結できればそれに越したことはないですが取り急ぎを想定して)
以上の流れを実現する為に

  • エディタ操作
  • ファイル操作
  • 外部アプリ操作
  • クリップボード操作(npmパッケージ呼び出し例)
  • VSCodeの基本設定に設定を追加して呼び出す
  • VSCodeの右クリックメニューにコマンド追加
  • VSCodeのデフォルトのショートカットキーを追加

をスニペット的に紹介します。

エディタ操作

エディタからテキストを取得 → (加工) → エディタに反映の流れ

まずはエディタ操作から。extension.tsの処理部に追記します。

TypeScript(extension.ts)
let editor = vscode.window.activeTextEditor; // エディタ取得
let doc = editor.document;            // ドキュメント取得
let cur_selection = editor.selection; // 選択範囲取得
if(editor.selection.isEmpty){         
    // 選択範囲が空であれば全てを選択範囲にする
    let startPos = new vscode.Position(0, 0);
    let endPos = new vscode.Position(doc.lineCount - 1, 10000);
    cur_selection = new vscode.Selection(startPos, endPos);
}

let text = doc.getText(cur_selection); //取得されたテキスト

/**
 * ここでテキストを加工します。
 **/

//エディタ選択範囲にテキストを反映
editor.edit(edit => {
    edit.replace(cur_selection, text);
});

選択範囲がある場合は、その範囲で取得→(テキスト加工)→同じ範囲でセット。
選択範囲がない場合は全体を取得→(テキスト加工)→セットすることができます。

これができるだけで、大分違うと思います。
SakuraEditor等で使われるコンバーター系のJavaScriptマクロがあればそのまま利用して完成ですね。

ファイル操作

ファイルの読み書きをしたい場合は以下の通りです。

TypeScript(extension.ts)
readFile("読み込み元のパス", (err, data) => {
    //dataにテキストが入っているので、読み込んだ後の処理をここに書く。
});
TypeScript(extension.ts)
writeFile("書き込み先のパス", "書き込みたいテキスト", (err) =>{
    //書き込み後の処理をここに書く。
});

ファイル操作については、VSCodeには既に搭載されているfsというパッケージを宣言する必要があります。搭載されているパッケージの宣言は以下の通りツールのアシスト機能を利用して行うことが可能です。
assist2.gif

ちなみにテンポラリパスを指定したいときはこんな感じで取れます。

TypeScript(extension.ts)
let uniqid = Math.random().toString(36).slice(-8); // ランダム文字による採番(被り考慮無)
let temp_path = tmpdir()+"/vscode-module-"+uniqid; // temp_pathにテンポラリファイルのパスが格納される。

外部アプリ操作

TypeScript(extension.ts)
exec("実行したいアプリのパス", (error,stdout,stderr) =>
{
    //実行した後の処理をここに書く。実行したアプリが終了した後に実行される。
}

execはchild_processのメソッドなので、[ファイル操作]の時と同様に宣言してください。

クリップボード操作(npmパッケージ呼び出し例)

クリップボード操作はVSCode標準のものが見つからなかった(多分・・)ので、
npm(node.js)のパッケージを利用しようと思います。(これをやってみたかっただけともいう。)
npmとは、一言でいうとJavaScriptライブラリのパッケージ管理をしてくれるツールです。「VSCodeが利用しているフレームワーク「Electron」もnpmの1パッケージだ」と言うと、その規模の大きさが分かると思います。

今回は 「copy-paste」というパッケージを拡張機能で使えるように導入したいと思います。

npmがインストールされた状態でコマンドプロンプト(ターミナルアプリ)を開き、開発環境のディレクトリ(「package.json」のあるフォルダ)まで移動し、

npm install copy-paste

と実行すると、目当ての開発環境向けに依存するパッケージごと全て導入し、使えるようにしてくれます。
使うときは、通例ではrequire()関数で指定します。もちろんその読み込み方もできるのですが、VSCode(TypeScript)では、文頭で

TypeScript(extension.ts)宣言部
import { paste } from 'copy-paste';

の様に宣言してやるとpasteという機能はそのファイル内で使いまわしができます。

TypeScript(extension.ts)
paste((e,data) => 
{
    //クリップボード取得後の処理をここに書く。
    //dataにクリップボードデータが入っている。
});

この一連を覚えると、VSCodeにない機能でもnpmから召喚して使うことができます。
https://www.npmjs.com/
しかし、パッケージ上はVSCodeとは並立関係なので依存関係には注意です。

VSCodeの操作

VSCode自体を操作したい場合はpackage.jsoncontributesという項目に追記します。

package.json例
{
    "contributes": {
        "configuration": {
            "type": "object",
            "title": "【モジュール名】",
            "properties": {
                "【モジュール名】.【設定名】": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "default": "",
                    "description": "【説明】"
                }
            }
        },
        "menus": {
            "editor/context": [{
                "command": "extension.【extension.jsで定義されたコマンド名】",
                "group": "9_cutcopypaste@500"
            }]
        },
        "keybindings": [{
            "command": "extension.【extension.jsで定義されたコマンド名】",
            "key": "【アサインするキー(「ctrl+alt+v」等)】"
        }]
    }
}

詳細の設定については英語ドキュメントですが、こちらをご参照。
https://code.visualstudio.com/docs/extensionAPI/extension-points

とりあえずさわりだけ説明します。

基本設定に設定を追加して呼び出す

VSCodeは基本設定もJSONで、変更したい場合はユーザー向のJSONを書くのですが、拡張機能の設定定義もそこに乗っかるように追加することができます。

package.json例
{
    "contributes": {
        "configuration": {
            "type": "object",
            "title": "【モジュール名】",
            "properties": {
                "【モジュール名】.【設定名】": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "default": "",
                    "description": "【説明】"
                }
            }
        }
    }
}

【モジュール名】はpackage.jsonのnameの値
【設定名】は設定のセクション名になるので、任意のアルファベットを指定。
【説明】は設定値の説明。わかりやすく書いた方がいいです。

上記設定を呼び出したい場合はこんな感じ

TypeScript(extension.ts)
const conf = vscode.workspace.getConfiguration('【モジュール名】');
const pathToMergeTool = ""+conf.get('【設定名】');

右クリックメニューにコマンド追加

拡張機能でエディタを右クリックしたときに出現するコンテキストメニューにコマンドを追加したい場合はこのように書きます。

package.json例
{
    "contributes": {
        "menus": {
            "editor/context": [
                {
                    "command": "extension.【extension.jsで定義したコマンド名】",
                    "group": "9_cutcopypaste@500"
                }
            ]
        }
    }
}

配列[~]の位置で複数設定することができます。
9_cutcopypasteというのはコンテキストメニューのブロック位置。任意に決定できるのですが、システムで決められているブロックを利用したいと思います

名前 説明
navigation 「宣言に移動」等、ナビゲーションに関するメニュー
1_modification コードフォーマッティング等、編集に関するメニュー
9_cutcopypaste 切り取り、貼り付けに関するメニュー

@以降の値はブロックの中の並び順です。

デフォルトのショートカットキーを追加

拡張機能でデフォルトのショートカットキーを定義することができます。

package.json例
{
    "contributes": {
        "keybindings": [
            {
                "command": "extension.【extension.jsで定義したコマンド名】",
                "key": "【アサインするキー(「ctrl+alt+v」等)】"
            }
        ]
    }
}

配列[~]の位置で複数設定することができます。
これは、見ての通りですね。

選択肢ダイアログ

その他、意外とつけるのが面倒だった印象があったものをご紹介。確か、選択肢をカスタマイズしようとして、てこずった記憶があります。

extend.ts
vscode.window.showInformationMessage("【質問】", { modal: true }, 'PATTERN-A', 'PATTERN-B')
.then(result => {
    if(result == "PATTERN-A"){
        //PATTERN-Aが選択された場合

    }
});

選択肢はコールバック引数(result)の文字列で一致させます。選択肢は「PATTERN-A.B..」の並びで追加します。

結果

今回はざっくりまとめてみましたが、これで次の一歩が踏み出せるきっかけになれば幸いです。

とりあえず外部プリプロセッサを利用した連携でこれまでのものを組み合わせて以下の様に書いてみました。
「プリプロセッサのパス」にエディタのテキストが入ったテンポラリファイルへのパスを引数(%E)としてセットされています。 プログラムを変えて利用できます。

extension.ts
'use strict';
import * as vscode from 'vscode';
import { writeFile, readFile } from 'fs';
import { tmpdir } from 'os';
import { exec } from 'child_process';

export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(vscode.commands.registerCommand('extension.external-process', () => {

        let toolPath = "notepad %E"; //プリプロセッサのパス

        let uniqid = Math.random().toString(36).slice(-8);

        let base_path = tmpdir()+"/"; //テンポラリディレクトリ
        let file_name_editor = "vscode-external-process-editor_"+uniqid+".txt"; //ファイル名
        let file_path_editor = base_path+file_name_editor; //テンポラリファイルパス

        let editor = vscode.window.activeTextEditor; // エディタ取得
        let doc = editor.document;            // ドキュメント取得
        let cur_selection = editor.selection; // 選択範囲取得

        if(editor.selection.isEmpty){
            // 選択範囲が空であれば全てを選択範囲にする
            let startPos = new vscode.Position(0, 0);
            let endPos = new vscode.Position(doc.lineCount - 1, 10000);
            cur_selection = new vscode.Selection(startPos, endPos);
        }

        let text = doc.getText(cur_selection); //取得されたテキスト

        /**
         * ここでテキストを加工します。
         **/

        //テンポラリファイル書き込み
        writeFile(file_path_editor, text, (err) =>{
            if(!err){
                //外部アプリ実行
                exec(toolPath.replace("%E", file_path_editor), (error,stdout,stderr) => {
                    if(!error){
                        //テンポラリファイル読み込み
                        readFile(file_path_editor,(err, data) => {
                            if(!err){
                                //エディタ選択範囲にテキストを反映
                                editor.edit(edit => {
                                    edit.replace(cur_selection, ""+data);
                                });
                            }else{
                                vscode.window.showInformationMessage("Error while read temporary file.");
                            }
                        });
                    }else{
                        vscode.window.showInformationMessage("Error while External Process.");
                    }
                });
            }else{
                vscode.window.showInformationMessage("Error while writing temporary file.");
            }
        });
    }));
}

あと、

merge-n-paste

https://marketplace.visualstudio.com/items?itemName=rawseq.merge-n-paste
こんなのを作りました。文字通り「マージしてペーストする」使い慣れた比較・マージツールが自由に使える拡張機能です。
merge-n-paste.gif