はじめに
VSCode の拡張機能を作ってみた。名前は何のひねりもなく OpenImageFile。その名の通り、Markdown から直接、画像ファイルを編集ソフトで開く拡張機能。なんてことはない拡張機能だけれども Markdown でマニュアル書いたりするときに効果絶大かと。
目次
ことのはじまり
最近、簡易マニュアルを Markdown で書いたのだけれども、マニュアルといえばスクショ。スクショといえばマニュアル。
あんまりにもスクショが多いので、やってらんないということで、なんか便利な拡張機能ないかしら、と調べてみたらありました、PasteImage。
この拡張機能はスクショを取った状態で、PasteImage すると自動的に保存して、マークダウンの記述も入れてくれるという素敵な拡張機能。
んなわけで、スクショとっては PasteImage しまくってご機嫌にマニュアルを作っていたのだけれど、ちょっと画像を編集してコメント入れたい、みたいな状況に置かれたときに、愕然とした。
PasteImage のファイル名は自動的に連番で振られるので、ファイル名見ただけでどのファイルかわからなくて、ファイル開くまでに手間がかかりすぎる!
というわけで、今度はカーソルの下にあるファイルを外部のアプリで開いてくれる拡張機能ないかしら、と調べてみたけど出てこない…。
無いなら仕方ない!、ということで作ることにしました、拡張機能。
使い方
-
OpenImageFile にアクセスして、
Install
をクリック。 - 開きたいファイルパスがある行にカーソルを移動して、コマンドパレット(Ctrl+Shift+P)から
OpenImageFile
を選択すると、デフォルトではペイントが開く。 - 以下の設定を変更することで、編集ソフトを変更できる(ただし、編集ソフトがコマンド起動できる必要はある)
Open Image File: Open Application
作った
以下、VSCode の拡張機能を初めて作ったはなし。使ってみたいだけならスルーでOK。
環境・前提条件
環境と作成にあたっての前提条件は以下の通り。
環境
- OS
- Windows 10 (Windows_NT x64 10.0.17763)
- エディタ
- Visual Studio Code バージョン: 1.32.3 (system setup)
前提条件
- Microsoftアカウントを持っている。
- GitHubアカウントを持っている。
VSCode 拡張機能開発 こと始め
所属する組織では基本 C++ とか C# とか Python あたりを触っているので、Typescript なにそれ?おいしいの? というところからスタート。
ともかく、開発環境を整えないことには話が始まらない、という事で以下のページを参考にしながら、何とか HelloWorld までたどり着く。
次に、Typescript の雰囲気をもう少し知っておきたいという事で、Github から適当にシンプルな機能の拡張機能を漁ってきた。具体的には以下のあたり。
- Sample Word Count extension for VS Code.
- An extension for Visual Studio Code that provides a configurable command for inserting the current date and time
あと、わからないなりに MS の APIリファレンスも合わせて読んだ。
コーディング
そんで、Typescriptの雰囲気はなんとなくわかったので、続いて作りたいものを考えてみる。ざっくり処理を考えてみたところ、API的なのが必要なのは、以下の3つ。
- カーソルがある行のテキスト取得
- 文字列の正規表現処理(テキストからファイルパスを抜くための)
- 外部コマンドの発行処理
1と2については、Typescript について調べていた時になんとなくわかっていたが、3が少し手こずった。調べた結果、以下のサイトに解決手段を見つけた。
これで、パーツはそろったので、作ってみることに。なにしろTypescript はじめてなので、ホントにこの書き方でいいのかという疑惑が常に付きまといつつも、ちょぼちょぼとコーディング。
特に、相対パスから絶対パスに変換する resolve
なんて、絶対にライブラリがあると思うのだけれども(path
の中の resolve
では機能不足だった)、どうやって調べりゃいいかもよくわからないので、とりあえずは自作。
で、出来上がったのは次のようなコード(ライブラリの調べ方は今後の課題)。
import {window, workspace} from 'vscode';
import * as child_process from 'child_process';
import * as path from 'path';
// メイン処理
export function execute()
{
let editor = window.activeTextEditor;
if(!editor){ return; }
let doc = editor.document;
if (doc.languageId === "markdown")
{
let nLine = editor.selection.active.line;
let linkMatchResult = doc.lineAt(nLine).text.match(/\!\[.*\]\((.*)\)/);
if(linkMatchResult && linkMatchResult.length === 2)
{
let filePath = linkMatchResult[1] ;
filePath = path.isAbsolute(filePath) ?
filePath :
resolve(path.dirname(doc.fileName), filePath);
executeCommand(filePath);
}
}
return;
}
// 相対パスから絶対パスに変換
export function resolve(baseDirPath: string, filePath: string)
{
let filePathElements = filePath.replace(/\\/gi, "/").replace(/\/$/, "").split("/");
let baseDirPathElements = baseDirPath.replace(/\\/gi, "/").replace(/\/$/, "").split("/");
let nUpDir = 0;
filePathElements.forEach(element => {
if(element==="..")
{
nUpDir++;
}
});
return (baseDirPathElements.length-nUpDir > 0) ?
baseDirPathElements.slice(0, baseDirPathElements.length-nUpDir).join("/") + "/" + filePathElements.slice(nUpDir, filePathElements.length).join("/") :
baseDirPathElements[0] + "/" + filePathElements.slice(nUpDir, filePathElements.length).join("/");
}
// アプリ起動のコマンド発行
export function executeCommand(filePath: string)
{
let execCommand = getOpenApplication() + " \"" + filePath.replace(/\//gi, "\\"); + "\"";
child_process.exec(execCommand, (error, stdout, stderror) => {});
}
// 設定(画像ファイルを開くアプリへのパス)を取得
export function getOpenApplication(): string
{
const openImageFileConfiguration = workspace.getConfiguration('openImageFile');
return openImageFileConfiguration.get("openApplication", "mspaint.exe");
}
で、これを openImageFile.ts
として保存。あとは、extension.ts
を次のようにして、いっちょ上がり(ホントか…)。
import {window, workspace, commands, Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextDocument} from 'vscode';
import * as openImageFile from './openImageFile';
export function activate(context: ExtensionContext)
{
let disposable = commands.registerCommand('openImageFile.openFile', () =>{
openImageFile.execute();
}
);
context.subscriptions.push(disposable);
}
export function deactivate() {}
あと、package.json
を以下を参考に編集した。コーディングとデバッグできるようになったら、何をどこに書けばいいのかは、割とすぐにわかるようになると思う。
試験する
とりあえず、書いてみたはいいものの色々怪しい。特に resolve()
が怪しい。怪しすぎる。というわけで、少なくとも正常系ではちゃんと動くことくらいは確認しておきたい。幸い yo code
したら extension.test.ts
ができていたので、ここに試験コードを書いて動きを確認。
import * as assert from 'assert';
import * as openImageFile from '../openImageFile';
suite("Open Image File Tests", function () {
test("resolve", function() {
assert.equal(openImageFile.resolve("C:\\hoge1\\hoge2\\hoge3\\hoge4\\hoge5\\hoge6", "../../../path.png"), "C:/hoge1/hoge2/hoge3/path.png");
assert.equal(openImageFile.resolve("C:\\hoge1\\hoge2\\hoge3\\hoge4\\hoge5\\hoge6\\", "../../../path.png"),"C:/hoge1/hoge2/hoge3/path.png");
// 以下同様に続く…
});
test("getOpenApplication", function(){
assert.equal(openImageFile.getOpenApplication(),"mspaint.exe");
});
});
内部の関数は extension.test.ts
に試験コード書いたのだけれど、UIが絡む execute()
の試験コードはどう書きゃいいんだよ!ってことでとりあえず自分が使いそうな範囲で Markdown を書いて、動作を確認。
あとは、1週間ほど使ってみて自分の使い方の範囲で問題がないことを確認。
公開する
折角作ったんだから、ということで GitHub と Marketplace に公開する事にした。
GitHub にソースを公開
これについては、別の記事にまとめた。公開した時の URL はこの後の package.json
の編集で使うのでメモっておく。
Marketplaceに公開
以下のサイトを参考にしながら拡張機能を Marletplace に公開した。この手の作業をするのが初めてだったってのもあって、正直ここが一番手間取った。
VSCode 拡張機能を作って公開してみた : non-italic-monokai
大まかな流れは以下の通り。
- Personal Access Token を作成
- Publisher を作成
- package.json を編集
- VSCE をインストール
- Marketplace に登録
Personal Access Token を作成
-
Visual Studio Team Services | Sign In にアクセスして、右上のメニューから
Security
を選択。
-
New Token
をクリックして、Name
とOrganization
とExpiriation
を入力。また、Scopes
をFull access
にしておく。ここでの注意はOrganization
をAll accessible organization
にしておくこと。 これを忘れててちょっとハマった…。で、Create
ボタンをクリック。
-
Personal Access Token
が発行されるので、Copy
ボタンをクリックして、別のところに保存しておく。この後Close
をクリックすると二度と取得できないので注意。
Publisher を作成
Manage Publishers & Extensions からパブリッシャーを作成。
package.json を編集
package.json
に以下を追加。
"repository": {
"type": "git",
"url": "<github の URL>"
},
"publisher": "<Publisher名>",
VSCE をインストール
拡張機能を公開するため用ツールとしてVSCEがMSから提供されているので、これをインストール。インストールは、コンソールを開いて以下を実行。
$ npm install -g vsce
Marketplace に登録
ここまでくれば、あとはもう少し。VSCode 拡張機能があるディレクトリでコンソールを開いて、以下のコマンドを実行
$ vsce login <Publisher名>
(PersonalAccessToken を入力する)
$ vsce publish
ちなみに、vsce login
でエラー403が出た場合は、Personal Access Token の Organization
が All accessible organization
になっていない可能性が高いので、設定を確認するとよい。
最後に
はじめて拡張機能開発から Marketplace への登録をやってみた。開発自体は思ってたよりは手軽にできるな、という印象(まぁ、大したもの作ってないんだから当たり前だけど)。登録も手こずりはしたものの、一回やってしまえばあとはどうという事はないな、という印象。
ドキュメント作成にかかる工数を削減したい、というモチベーションは自分の中で根強くあるので、今後もちょぼちょぼと軽い拡張機能を作っていきたいと思う。(なお、念のために書いておくと、文章を打ち込む作業であったり、図表作成であったり、と思考以外の、作業の部分を効率化したい、というキモチ)