画像ファイルを外部アプリで開く拡張機能 <VSCode拡張機能開発～公開まで>

# はじめに

VSCode の拡張機能を作ってみた。名前は何のひねりもなく OpenImageFile。その名の通り、Markdown から直接、画像ファイルを編集ソフトで開く拡張機能。なんてことはない拡張機能だけれども Markdown でマニュアル書いたりするときに効果絶大かと。


![OpenImageFile.gif](https://qiita-image-store.s3.amazonaws.com/0/119094/14bfa378-b0da-3ce6-8e68-dbf62b150d78.gif)


## 目次
* [はじめに](#はじめに)
	* [目次](#目次)
	* [ことのはじまり](#ことのはじまり)
	* [使い方](#使い方)
* [作った](#作った)
	* [環境・前提条件](#環境前提条件)
		* [環境](#環境)
		* [前提条件](#前提条件)
	* [VSCode 拡張機能開発 こと始め](#vscode-拡張機能開発-こと始め)
	* [コーディング](#コーディング)
	* [試験する](#試験する)
* [公開する](#公開する)
	* [GitHub にソースを公開](#github-にソースを公開)
	* [Marketplaceに公開](#marketplaceに公開)
		* [Personal Access Token を作成](#personal-access-token-を作成)
		* [Publisher を作成](#publisher-を作成)
		* [package.json を編集](#packagejson-を編集)
		* [VSCE をインストール](#vsce-をインストール)
		* [Marketplace に登録](#marketplace-に登録)
* [最後に](#最後に)



## ことのはじまり 

最近、簡易マニュアルを Markdown で書いたのだけれども、マニュアルといえばスクショ。スクショといえばマニュアル。
あんまりにもスクショが多いので、やってらんないということで、なんか便利な拡張機能ないかしら、と調べてみたらありました、[PasteImage](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)。
この拡張機能はスクショを取った状態で、PasteImage すると自動的に保存して、マークダウンの記述も入れてくれるという素敵な拡張機能。

んなわけで、スクショとっては PasteImage しまくってご機嫌にマニュアルを作っていたのだけれど、ちょっと画像を編集してコメント入れたい、みたいな状況に置かれたときに、愕然とした。
PasteImage のファイル名は自動的に連番で振られるので、ファイル名見ただけでどのファイルかわからなくて、ファイル開くまでに手間がかかりすぎる！

というわけで、今度はカーソルの下にあるファイルを外部のアプリで開いてくれる拡張機能ないかしら、と調べてみたけど出てこない…。

無いなら仕方ない！、ということで作ることにしました、拡張機能。

## 使い方

1. [OpenImageFile](https://marketplace.visualstudio.com/items?itemName=dreamChaser0105.vscode-openimagefile) にアクセスして、`Install` をクリック。
2. 開きたいファイルパスがある行にカーソルを移動して、コマンドパレット(Ctrl+Shift+P)から`OpenImageFile`を選択すると、デフォルトではペイントが開く。
3. 以下の設定を変更することで、編集ソフトを変更できる（ただし、編集ソフトがコマンド起動できる必要はある）
    `Open Image File: Open Application`

![OpenImageFile.gif](https://qiita-image-store.s3.amazonaws.com/0/119094/14bfa378-b0da-3ce6-8e68-dbf62b150d78.gif)



# 作った
以下、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 までたどり着く。

- [Visual Studio Code はじめての拡張機能開発](https://qiita.com/rma/items/8c53077d1355ab8fa4c6)

次に、Typescript の雰囲気をもう少し知っておきたいという事で、Github から適当にシンプルな機能の拡張機能を漁ってきた。具体的には以下のあたり。

- [Sample Word Count extension for VS Code.](https://github.com/Microsoft/vscode-wordcount)
- [An extension for Visual Studio Code that provides a configurable command for inserting the current date and time](https://github.com/jsynowiec/vscode-insertdatestring)

あと、わからないなりに MS の APIリファレンスも合わせて読んだ。

- [VS Code API](https://code.visualstudio.com/api/references/vscode-api)


## コーディング

そんで、Typescriptの雰囲気はなんとなくわかったので、続いて作りたいものを考えてみる。ざっくり処理を考えてみたところ、API的なのが必要なのは、以下の３つ。

1. カーソルがある行のテキスト取得
2. 文字列の正規表現処理(テキストからファイルパスを抜くための)
3. 外部コマンドの発行処理
4. 
1と2については、Typescript について調べていた時になんとなくわかっていたが、3が少し手こずった。調べた結果、以下のサイトに解決手段を見つけた。

- [Visual Studio Codeの拡張機能で外部コマンドを実行したい](https://blog.okazuki.jp/entry/2016/08/29/222739)

これで、パーツはそろったので、作ってみることに。なにしろTypescript はじめてなので、ホントにこの書き方でいいのかという疑惑が常に付きまといつつも、ちょぼちょぼとコーディング。

特に、相対パスから絶対パスに変換する `resolve` なんて、絶対にライブラリがあると思うのだけれども(`path` の中の `resolve` では機能不足だった)、どうやって調べりゃいいかもよくわからないので、とりあえずは自作。

で、出来上がったのは次のようなコード(ライブラリの調べ方は今後の課題)。


```typescript
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` を次のようにして、いっちょ上がり（ホントか…）。

```typescript

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` を以下を参考に編集した。コーディングとデバッグできるようになったら、何をどこに書けばいいのかは、割とすぐにわかるようになると思う。

- [An extension for Visual Studio Code that provides a configurable command for inserting the current date and time](https://github.com/jsynowiec/vscode-insertdatestring)


## 試験する
とりあえず、書いてみたはいいものの色々怪しい。特に `resolve()` が怪しい。怪しすぎる。というわけで、少なくとも正常系ではちゃんと動くことくらいは確認しておきたい。幸い `yo code` したら `extension.test.ts` ができていたので、ここに試験コードを書いて動きを確認。

```typescript
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 を書いて、動作を確認。

あとは、１週間ほど使ってみて自分の使い方の範囲で問題がないことを確認。

# 公開する

折角作ったんだから、ということで GitHub と Marketplace に公開する事にした。

## GitHub にソースを公開
これについては、別の記事にまとめた。公開した時の URL はこの後の `package.json` の編集で使うのでメモっておく。

[ソースコードを GitHub に公開する方法](https://qiita.com/UKawamura/items/aece0ecbdbcdd6c95d17)


## Marketplaceに公開
以下のサイトを参考にしながら拡張機能を Marletplace に公開した。この手の作業をするのが初めてだったってのもあって、正直ここが一番手間取った。

[VSCode 拡張機能を作って公開してみた : non-italic-monokai](http://neos21.hatenablog.com/entry/2018/05/05/080000#VSCode-%E6%8B%A1%E5%BC%B5%E6%A9%9F%E8%83%BD%E3%82%92%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E6%BA%96%E5%82%99)

大まかな流れは以下の通り。

1. Personal Access Token を作成
2. Publisher を作成
3. package.json を編集
4. VSCE をインストール
5. Marketplace に登録


### Personal Access Token を作成
1. [Visual Studio Team Services | Sign In](https://go.microsoft.com/fwlink/?LinkId=307137) にアクセスして、右上のメニューから `Security` を選択。
![2019-03-24-12-51-55.png](https://qiita-image-store.s3.amazonaws.com/0/119094/b19af5e0-1723-400d-43eb-6cd0032d7e51.png)


2. `New Token` をクリックして、`Name` と `Organization` と `Expiriation` を入力。また、`Scopes` を `Full access` にしておく。**ここでの注意は `Organization` を `All accessible organization` にしておくこと。** これを忘れててちょっとハマった…。で、`Create` ボタンをクリック。
![2019-03-24-14-01-42.png](https://qiita-image-store.s3.amazonaws.com/0/119094/8b897ff9-5755-0970-7248-0a77d4112a12.png)



3. `Personal Access Token` が発行されるので、`Copy` ボタンをクリックして、別のところに保存しておく。この後 `Close` をクリックすると二度と取得できないので注意。
![2019-03-24-12-55-57.png](https://qiita-image-store.s3.amazonaws.com/0/119094/5458237d-1b60-108e-9cc3-816ef3c5fb45.png)



### Publisher を作成
[Manage Publishers & Extensions](https://marketplace.visualstudio.com/manage) からパブリッシャーを作成。

![2019-03-24-14-14-59.png](https://qiita-image-store.s3.amazonaws.com/0/119094/e6674da4-3c9b-8cbb-4875-b48347044222.png)


### package.json を編集
`package.json` に以下を追加。

```json
"repository": {
	"type": "git",
	"url": "<github の URL>"
},
"publisher": "<Publisher名>",
```

### VSCE をインストール
拡張機能を公開するため用ツールとしてVSCEがMSから提供されているので、これをインストール。インストールは、コンソールを開いて以下を実行。

```shell
$ npm install -g vsce
```

### Marketplace に登録
ここまでくれば、あとはもう少し。VSCode 拡張機能があるディレクトリでコンソールを開いて、以下のコマンドを実行

```shell
$ vsce login <Publisher名>
(PersonalAccessToken を入力する)
$ vsce publish
```

ちなみに、`vsce login` でエラー403が出た場合は、Personal Access Token の `Organization` が `All accessible organization` になっていない可能性が高いので、設定を確認するとよい。

# 最後に
はじめて拡張機能開発から Marketplace への登録をやってみた。開発自体は思ってたよりは手軽にできるな、という印象（まぁ、大したもの作ってないんだから当たり前だけど）。登録も手こずりはしたものの、一回やってしまえばあとはどうという事はないな、という印象。

ドキュメント作成にかかる工数を削減したい、というモチベーションは自分の中で根強くあるので、今後もちょぼちょぼと軽い拡張機能を作っていきたいと思う。（なお、念のために書いておくと、文章を打ち込む作業であったり、図表作成であったり、と思考以外の、作業の部分を効率化したい、というキモチ）