はじめに
自分は2021年に新卒でWeb系の開発会社にフロントエンジニアとして入社し2022年で2年目になります。クソアプリカレンダーの14日目を担当します。
今回はTypeScriptでもお馴染みの「any」を使うとプレバトのあの先生が赤ペンを入れてくれるVSCode拡張機能の「any-checker」を開発しました。
成果物
下記のようにany
を記述した状態で拡張機能を起動しコードを保存すると、
プレバトでもお馴染みのあの先生が叱ってany
に赤ペンを入れてくれます。
この記事で学べること
- VSCode拡張機能の開発方法を基礎から学べる
クソアプリといえど何かしらの学びを共有したいので、開発方法を基礎から解説します。
これからVSCode拡張機能開発に挑戦してみたいしてみたい方はぜひ参考にしていただければと思います。
おことわり
- あくまでクソアプリなのでアプリの実用性の保証はないです(any依存症を治せるかも)
- 今後追加したい機能については最後の章に追記しています
- 「完成」を第一に考えていたので紹介するコードにはまだリファクタリングに余地があります
なおVSCode拡張機能まとめの記事も書いているので合わせて読んでいただけると嬉しいです。
環境構築と準備
まず初めに公式ドキュメントにも掲載されているVSCodeの拡張機能チュートリアルを解説していきます。
環境構築
公式ドキュメントに従って簡易的な拡張機能をまずは開発していきます。
まず拡張機能開発に必要なツールをnpm
でインストールします。
$ npm install -g yo generator-code
次にVSCodeの拡張機能開発をするための雛形を作成します。該当のディレクトリ上で下記のコマンドを実行します。
$ yo code
下記の質問が出てくるので回答していきます。
What type of extension do you want to create?
--New Extension (TypeScript)を選択
What's the name of your extension?
--拡張機能名を入力 (今回はany-checker)
What's the identifier of your extension?
--何も入れずにエンター
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を選択 (yarnでも大丈夫です)
インストール中に下記の質問がされます。
Do you want to open the new folder with Visual Studio Code?
--Open with `code`を選択します
下記のディレクトリが作成されていればOKです。
Hello Worldを表示する
VSCodeのメニューにHelloを追加し、選択すると「Hello World」という文字が出てくるような拡張機能を最初に開発していきます。
VSCodeの拡張機能開発はsrc/extension.ts
とpackage.json
の2つのファイルに処理記述をしていきます。
package.jsonの記述
package.json
には処理を呼び出すための操作を記述していきます。
今回はメニューに「Hello」と追加してクリックされた際にextension.ts
(後で記述)の処理が実行されるような設定を記述します。
【該当箇所のみ抜粋】
"contributes": {
"commands": [
{
"command": "vscode-context.Hello",
"title": "Hello"
}
],
"menus": {
"editor/context": [
{
"when": "editorHasSelection",
"command": "vscode-context.Hello",
"group": "myGroup@1"
}
]
}
},
-
commands
で⇧⌘P
(F5)を押した際に呼び出すコマンドを設定します。 -
menus
でVSCodeのメニューに表示されるコマンドを設定します。
コンパイル(後で解説)すると下記のように確認できます。
extension.tsの記述
次に先ほどpackage.jsonに登録したHelloコマンドを実行した時に、行われる処理を記述していきます。
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
// package.jsonに追記したコマンドを設定(今回は「vscode-context.Hello」)
let cmd = vscode.commands.registerCommand("vscode-context.Hello", () => {
customContext("vscode-context.Hello");
});
context.subscriptions.push(cmd);
}
// コマンドの内容はkeyに格納されている
const customContext = (key: string) => {
// Helloコマンドが実行された場合の処理
if ((key = "vscode-context.Hello")) {
vscode.window.showInformationMessage("Hello World");
}
};
export function deactivate() {}
コンパイルと動作確認
記述した内容をコンパイルしVSCodeのデバックで動作がうまくいっているかを確認します。
$ npm run compile
⇧⌘P
を押して「Debug: Debug npm Script
> vscode:prepublish
」を選択します。
するとVSCodeの左下に「Run Extension」が表示されるのでクリックします。(別タブでVSCodeが開かれる)
ファイルを作成して適当に記述をし、記述した文字を選択してメニューの中から「Hello」を選択します。
Helloをクリックすると右下に「Hello World」という文字が出力されればOKです。
同様に⇧⌘P
からHelloと入力しても同じ処理が実行されることが確認できます。
これでチュートリアルは完了です。次の章から具体的にアプリを開発していきます。
クソアプリの開発
ここからは今回開発するクソアプリの処理内容をsrc/extension.ts
に記述していきます。
下記の手順で下記発を進めていきます
- プレバトの先生の画像を表示する
- anyの個数に応じて警告文を変える
- anyという文字があったら赤線を引く
package.jsonにコマンドを登録する
今回はメニューに「any-checker」を追加し選択されると拡張機能が呼び出されるような挙動にします。
チュートリアルでの説明と同様にpackage.jsonにコマンドを追加します。
"activationEvents": [
"onCommand:vscode-context.any-check"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "vscode-context.any-check",
"title": "anyチェック"
}
],
"menus": {
"editor/context": [
{
"when": "editorHasSelection",
"command": "vscode-context.any-check",
"group": "myGroup@4"
}
]
}
},
このように登録することデバックを起動した時に下記のようにメニューに「anyチェック」が追加されています。(後で説明)
コマンドに関する準備は以上です。
画像を表示する
VSCodeの拡張機能ががメニューから呼び出された時にプレバトの先生が表示される処理をsrc/extension.ts
に記述していきます。
画像のプレビューは公式ドキュメントのWebview APIを参考に実装を進めました。
コメントアウトとともに画像をプレビューするための記述は下記の通りです。
export const activate = (context: vscode.ExtensionContext) => {
// package.jsonに記述したコマンドの登録
let cmd = vscode.commands.registerCommand("vscode-context.any-check", () => {
customContext("vscode-context.any-check");
});
context.subscriptions.push(cmd);
};
// 画像呼び出し用のHTMLを記述
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<img src="https://www.mbs.jp/mbs-column/p-battle/thumb/20190924175210-dfacb10b6a288de4a8506b3c24a433dd3684b841.jpg" />
</body>
</html>`;
}
const customContext = (key: string) => {
const editor = vscode.window.activeTextEditor;
// 「anyチェック」が選択された時に処理を開始
if (editor && key === "vscode-context.any-check") {
// 別タブでgetWebviewContentで定義したHTMLを呼び出し
const panel = vscode.window.createWebviewPanel(
"t",
`t`,
vscode.ViewColumn.Beside,
{}
);
panel.webview.html = getWebviewContent();
}
};
画像表示に使ったcreateWebviewPanel
に関しては公式ドキュメントで詳しく解説されています。
anyがあったら赤線を引く
anyという文字列の上に付与する赤線のスタイルをcreateTextEditorDecorationType
を用いて定義します。
/* anyの上に付与するデコレーション(赤色打ち消し線) */
const decorationType = vscode.window.createTextEditorDecorationType({
textDecoration: "line-through;",
color: "red;",
});
次にanyという文字があった場合に上記の赤線を文字の上に追加する処理を記述します。
コードを1行づつ取得してループ処理でanyという文字があるかを確認しています。
/* anyがエディター内にあった場合に赤線を引く */
const decorate = (editor: vscode.TextEditor) => {
const sourceCode = editor.document.getText();
/* 適応したい任意の文字を入れる */
const regex = /(any)/;
const decorationsArray: vscode.DecorationOptions[] = [];
/* 1行ごとにソースコードを取得する */
const sourceCodeArr = sourceCode.split("\n");
let anyCount = 0;
for (let line = 0; line < sourceCodeArr.length; line++) {
const match = sourceCodeArr[line].match(regex);
if (match !== null && match.index !== undefined) {
let range = new vscode.Range(
new vscode.Position(line, match.index),
new vscode.Position(line, match.index + match[1].length)
);
anyCount += 1;
decorationsArray.push({ range });
}
}
};
警告文を出す
最後にanyCount
に応じて出力される文字が変わる処理を記述します。
ここでは先程のチュートリアルでも説明したshowInformationMessage
を利用します。
/* anyがエディター内にあった場合に赤線を引く */
const decorate = (editor: vscode.TextEditor) => {
// 省略
const textLabel = anyCount > 1 ? "いい加減にしなさい" : "あっぱれ";
vscode.window.showInformationMessage(`${textLabel}`, {
modal: true,
});
editor.setDecorations(decorationType, decorationsArray);
};
【完成系コード】
import * as vscode from "vscode";
/* anyの上に付与するデコレーション(赤色打ち消し線) */
const decorationType = vscode.window.createTextEditorDecorationType({
textDecoration: "line-through;",
color: "red;",
});
/* anyがエディター内にあった場合に赤線を引く */
const decorate = (editor: vscode.TextEditor) => {
const sourceCode = editor.document.getText();
/* 適応したい任意の文字を入れる */
const regex = /(any)/;
const decorationsArray: vscode.DecorationOptions[] = [];
/* 1行ごとにソースコードを取得する */
const sourceCodeArr = sourceCode.split("\n");
let anyCount = 0;
for (let line = 0; line < sourceCodeArr.length; line++) {
const match = sourceCodeArr[line].match(regex);
if (match !== null && match.index !== undefined) {
let range = new vscode.Range(
new vscode.Position(line, match.index),
new vscode.Position(line, match.index + match[1].length)
);
anyCount += 1;
decorationsArray.push({ range });
}
}
const textLabel = anyCount > 1 ? "いい加減にしなさい" : "あっぱれ";
vscode.window.showInformationMessage(`${textLabel}`, {
modal: true,
});
editor.setDecorations(decorationType, decorationsArray);
};
export const activate = (context: vscode.ExtensionContext) => {
let cmd = vscode.commands.registerCommand("vscode-context.any-check", () => {
customContext("vscode-context.any-check");
});
context.subscriptions.push(cmd);
};
// 画像呼び出し用のHTMLを記述
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<img src="https://www.mbs.jp/mbs-column/p-battle/thumb/20190924175210-dfacb10b6a288de4a8506b3c24a433dd3684b841.jpg" />
</body>
</html>`;
}
const customContext = (key: string) => {
const editor = vscode.window.activeTextEditor;
// 「anyチェック」が選択された時に処理を開始
if (editor && key === "vscode-context.any-check") {
const panel = vscode.window.createWebviewPanel(
"t",
`t`,
vscode.ViewColumn.Beside,
{}
);
panel.webview.html = getWebviewContent();
// 色つける
vscode.workspace.onWillSaveTextDocument((event) => {
const openEditor = vscode.window.visibleTextEditors.filter(
(editor) => editor.document.uri === event.document.uri
)[0];
decorate(openEditor);
});
}
};
export function deactivate() {}
{
"name": "vscode-context",
"displayName": "vscode-context",
"description": "custom context",
"version": "0.0.1",
"engines": {
"vscode": "^1.69.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:vscode-context.any-check"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "vscode-context.any-check",
"title": "anyチェック"
}
],
"menus": {
"editor/context": [
{
"when": "editorHasSelection",
"command": "vscode-context.any-check",
"group": "myGroup@4"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js"
},
"devDependencies": {
"@types/glob": "^7.2.0",
"@types/mocha": "^9.1.1",
"@types/node": "16.x",
"@types/vscode": "^1.69.0",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"@typescript-eslint/parser": "^5.30.0",
"@vscode/test-electron": "^2.1.5",
"eslint": "^8.18.0",
"glob": "^8.0.3",
"mocha": "^10.0.0",
"typescript": "^4.7.4",
"vsce": "^2.9.2"
}
}
F5(⌘+shift+p)をクリックしタブに「Debug npm Script」を入れてデバック機能を起動させます。
左下のRun Extension
をクリックし開発した拡張機能の動作を確認します。
メニューからanyチェックを選択すると「any」に赤ペンが入っていることが確認できます。
最後に
いかがだったでしょうか。今回はVSCodeの拡張機能をクソアプリを開発しながら学ぶ記事を書きました。
今回開発した拡張機能はまだ実用的でないので、下記の機能を追加した上でリリースしようと思っています。
- anyではない型候補を表示してくれる
- 型定義のany以外は反応しないようにする(変数名など)
- コードのリファクタリング
普段はフロントエンド及び、サーバーなど幅広く記事を書いているのでぜひ読んでいただけると嬉しいです。