仕事をちょっぴり楽にするVSCode拡張機能を作ろう！　メモファイル作成編

[フューチャー Advent Calendar 2022](https://qiita.com/advent-calendar/2022/future) 12日目の記事です。    
昨日は @2019Shun さんの「[Excelファイルを読み込むPythonライブラリを比較してみた](https://qiita.com/2019Shun/items/db0028a535297ef50184)」でした。

## はじめに

はじめまして。フューチャーの @920OJ と申します。アルバイトで2年弱勤務した後今年の7月に新卒入社し、新人研修を経て、最近PJに配属されたばかりの新人です。日々の業務に苦戦しつつも、刺激的な毎日を送っています。

今回は、「仕事をちょっぴり楽にするVSCode拡張機能を作ろう！」というテーマで、メモファイルを簡単に作成できるVisual Studio Code（以下VSCode）の拡張機能を作成する方法を紹介します。

### 環境

- Windows 11 Home Edition
- Visual Studio Code 1.74.0
- Node.js v18.12.1
- npm v8.19.2

## 完成したもの

まずはGIFをご覧ください。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462753/5bff16f7-d129-eb96-0fd9-676e16fb232d.gif)

コマンドパレットでCreate Memo File を選択すると入力欄が表示されます。メモのタイトルを入力しEnterを押すと、「日付_タイトル.md」というMarkdownファイルが作成されます。また、このMarkdownファイルは先程入力したタイトルの見出し1が設定されています。

### 作った経緯

私はよく業務中のメモをMarkdown形式で、VSCodeを利用して取るようにしています。そんな中、毎回「日付_タイトル.md」というファイルを作成し、見出し1を記載し、というのはたかが10秒程度の作業ですが、同じタイトルを2回入力するため面倒に感じるときがあります。そこで、このファイルを用意する作業を拡張機能で半自動化すれば、少しは労力が軽減されるのでは、と考えました。（10秒が5秒に変わるだけでは、というツッコミは無いものとします）

後は、単純にVSCodeの拡張機能がどのように作られているかを知りたかったためです。では早速作っていきましょう！

## 環境構築

まずは、VSCodeの拡張機能を作成するために必要なものを導入します。モダンWebアプリ総合開発ツールであるYeomanのうちの一機能である雛形生成機能 `yo` と、VSCodeが公式に提供している拡張機能コードジェネレータである `VS Code Extension Generator` を、npmを利用してグローバルにインストールします。

```bash
npm install -g yo generator-code
```

次に、`yo` コマンドを利用して、VSCodeの拡張機能の雛形を生成します。ここでは、カレントディレクトリに `memo-starter` ディレクトリを作成した上で、その中に生成しています。

```bash
yo code memo-starter
```

前段で正常にインストールが完了していると、AAで表現されたおじさま（Yeoman）が表示されます。少し和みますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462753/4f5f8c9d-d7f5-231e-7506-42da21527fa5.png)

あとは質問に答えていきます。今回はTypeScriptで記述するのと、Gitレポジトリとして初期化することを選択しました。

```
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? memo-starter
? What's the identifier of your extension? memo-starter
? What's the description of your extension?
? Initialize a git repository? Yes
? Bundle the source code with webpack? Yes
? Which package manager to use? npm
```

ここまで答えると、自動で必要なライブラリのインストール等が実施されます。最後に、自動でVSCodeを開くか聞いてくるので、 `Open with 'Code'` を選択しましょう。

```
Writing in C:\Users\920oj\Projects\Code\memo-starter...
   create memo-starter\.vscode\extensions.json
   create memo-starter\.vscode\launch.json
   create memo-starter\.vscode\settings.json
   create memo-starter\.vscode\tasks.json
   create memo-starter\package.json
   create memo-starter\tsconfig.json
   create memo-starter\.vscodeignore
   create memo-starter\webpack.config.js
   create memo-starter\vsc-extension-quickstart.md
   create memo-starter\.gitignore
   create memo-starter\README.md
   create memo-starter\CHANGELOG.md
   create memo-starter\src\extension.ts
   create memo-starter\src\test\runTest.ts
   create memo-starter\src\test\suite\extension.test.ts
   create memo-starter\src\test\suite\index.ts
   create memo-starter\.eslintrc.json

Changes to package.json were detected.

Running npm install for you to install the required dependencies.

added 303 packages, and audited 304 packages in 28s

58 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Your extension memo-starter has been created!

To start editing with Visual Studio Code, use the following commands:

     code memo-starter

Open vsc-extension-quickstart.md inside the new extension for further instructions
on how to modify, test and publish your extension.

To run the extension you need to install the recommended extension 'amodio.tsl-problem-matcher'.

For more information, also visit http://code.visualstudio.com and follow us @code.


? Do you want to open the new folder with Visual Studio Code?
> Open with `code`
  Skip
```

このように、拡張機能を作成する前準備が整いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462753/91488c85-a37a-a6f9-1c8d-5b77b5d7c7b6.png)

### 開発中の拡張機能の実行方法

この雛形には、拡張機能開発中にプレビューするためのデバッグ構成が含まれているため、すぐに拡張機能のデバッグを開始することができます。F5を押すと、新しいウィンドウが開くので、`Ctrl + Shift + P` を押下し、 `Hello World` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462753/180d52fd-e894-7dca-84e6-0a033ac435bb.png)

右下に「Hello World from memo-starter!」と表示されていたら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462753/e55cc3e4-6679-124d-8189-0222e17a2290.png)

ちなみに、これを実現しているコードは以下の通りです。package.jsonで指定しているコマンド `memo-starter.helloWorld` に対して、中身を定義していることがわかります。

```json:package.json
  "contributes": {
    "commands": [
      {
        "command": "memo-starter.helloWorld",
        "title": "Hello World"
      }
    ]
  },
```

```typescript:extensions.ts
	// The command has been defined in the package.json file
	// Now provide the implementation of the command with registerCommand
	// The commandId parameter must match the command field in package.json
	let disposable = vscode.commands.registerCommand('memo-starter.helloWorld', () => {
		// The code you place here will be executed every time your command is executed
		// Display a message box to the user
		vscode.window.showInformationMessage('Hello World from memo-starter!');
	});

	context.subscriptions.push(disposable);
```

## コードを書いていく

今回実現したい要件を書き出します。ざっとこんな感じです。

1. コマンドパレットを開き、「create memo file」を選択する
2. メモのタイトルとなる文字列を入力する
3. VSCodeで開いているディレクトリ上に、「YYYYMMDD_2で入力した文字列.md」ファイルが出来上がる
    - 中身の1行目に2で入力した文字列が見出し1として入力されている

### package.json にコマンドを登録する

先述の通り、package.json

```json:pakcage.json
  "contributes": {
    "commands": [
      {
        "command": "memo-starter.createMemoFile",
        "title": "Create Memo File"
      }
    ]
  },
```

### extensions.ts を記入する

以下に `activate()` 関数を記載します。

```typescript
export function activate(context: vscode.ExtensionContext) {
	// createMemoFileCommand メモファイル生成用
	const createMemoFileCommand = vscode.commands.registerCommand('memo-starter.createMemoFile', async () => {
		// VSCodeで開いているディレクトリを取得
		// 開いていない場合はエラーを出して終了する
		const folders = vscode.workspace.workspaceFolders;
		if (folders === undefined) {
			vscode.window.showErrorMessage('Error: Open the folder before executing this command.');
			return;
		}
		const folderPath = folders[0].uri;

		// ファイル名入力
		const inputName = await vscode.window.showInputBox();

		// ファイル名構築
		// フォーマットは YYYYMMDD_入力した文字列.md とする
		const date = new Date();
		const fileName = `${date.getFullYear()}${("00" + String(date.getMonth()+1)).slice(-2)}${("00" + String(date.getDate())).slice(-2)}_${inputName}.md`;

		// 書き込むファイルのフルパスを生成
		const fullUri = vscode.Uri.joinPath(folderPath, fileName);
		
		// 書き込むファイルの内容を準備
		// Uint8Arrayに変換する
		const content = `# ${inputName}`;
		const blob: Uint8Array = Buffer.from(content);

		// ファイル書き込み
		await vscode.workspace.fs.writeFile(fullUri, blob);
	});

	context.subscriptions.push(createMemoFileCommand);
}
```

いくつかフォーカスして説明します。

#### 現在VSCodeで開いているディレクトリのパスを取得する

今回は、VSCodeで開いているディレクトリにファイルを新規作成します。今回初めて知ったのですが、VSCodeには「ワークスペース」という概念があり、「フォルダを開く」と、ワークスペースで1つのフォルダを読み込むという動作が取られているようです。ワークスペースは複数のディレクトリを読み込ませることができるそうですが、ここでは1ワークスペースに1ディレクトリという構成のみを考えることとします。

以下のように、 `vscode.workspace.workspaceFolders` を利用することで、ワークスペースで開いているディレクトリの情報を取得します。ワークスペースでディレクトリが開かれていれば配列を、開かれていない場合はundefinedになるため、開かれていない場合はエラーメッセージを表示します。

```typescript
		// VSCodeで開いているディレクトリを取得
		// 開いていない場合はエラーを出して終了する
		const folders = vscode.workspace.workspaceFolders;
		if (folders === undefined) {
			vscode.window.showErrorMessage('Error: Open the folder before executing this command.');
			return;
		}
		const folderPath = folders[0].uri;
```

ワークスペースでディレクトリが開かれていない場合は、以下のようにエラーメッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462753/a2150d5f-e068-6583-d1cc-1a4271084027.png)

#### ファイルを書き込む

ファイルを書き込むためには、Node.jsで提供されている `fs` ではなく、VSCodeのAPIとして用意されている `vscode.workspace.fs` を利用します。

まずは、書き込むファイルのフルパスを生成します。書き込む際に利用する `vscode.workspace.fs.writeFile` では、第一引数に `vscode.Uri` 型を指定する必要があります。そこで、先程取得したワークスペースで開かれているディレクトリのUri情報（`vscode.Uri`型）と、ファイル名（`String`型）を結合するために、`vscode.Uri.joinPath()` メソッドを利用します。

```typescript
        const folderPath = editor[0].uri; // vscode.Uri型のオブジェクト
        
        /** （中略）**/

        const fileName = `${date.getFullYear()}${("00" + String(date.getMonth()+1)).slice(-2)}${("00" + String(date.getDate())).slice(-2)}_${inputName}.md`; // String型

        // 書き込むファイルのフルパスを生成
		const fullUri = vscode.Uri.joinPath(folderPath, fileName); // vscode.Uri型とString型を結合し、vscode.Uri型を返す
```

さらに、書き込むファイルの内容は `Uint8Array` （バイト列）にする必要があります。 `Buffer.from()` を利用して変換します。最後に、 `vscode.workspace.fs.writeFile()` を呼び出して書き込み完了です。

```typescript
		// 書き込むファイルの内容を準備
		// Uint8Arrayに変換する
		const content = `# ${inputName}`;
		const blob: Uint8Array = Buffer.from(content);

		// ファイル書き込み
		await vscode.workspace.fs.writeFile(fullUri, blob);
```

## まとめ

意外と簡単に作ることができました。APIも豊富に用意されているため、様々なことを拡張機能で実現できそうな感触でした。

今回は時間が無かったので簡単なものになってしまいましたが、今度はテンプレートファイルを登録する機能を追加したり、テストコードを書いたり（大事）に、挑戦してみたいと思います。

### 参考文献

https://code.visualstudio.com/api/get-started/your-first-extension

https://code.visualstudio.com/api/references/vscode-api

https://qiita.com/YuichiNukiyama/items/ef16a0219f46ea03a045

https://bgt-48.blogspot.com/2021/10/visual-studio-code.html



---

明日は @kazupc さんの「Random Network Distillation 徹底解説」です。