Awakening Extension (拡張開発はじまるよ🔰)

Event:

VS Code Conference Japan 2022 - 2023

Presented:

2023/01/21 nikkie 14:35-

Thank you VSCodeConJP❤️

  • ㊗️ハイブリッド開催🎉

  • 15分2本発表の機会をありがとうございます

  • 2021開催がなければ、この発表は実現していません!

拡張開発はじまるよ🔰

過去のVS Code Conferenceのハンズオンテキストの内容を改造し、その拡張機能を公開するまでの
ありのまま(成功も失敗も両方)の記録である。

イベントサイト より

拡張開発の流れを追体験

  • VS Code拡張開発の経験がない方(過去の私 含む)向け

  • 開発〜公開までの流れ・全体感を共有

  • 「拡張開発やってみよう」と思っていただけたら🙌

お前、誰よ(知ってる)

  • わわわ、わたし、にっきー

  • @ftnext @ftnextはてなブログ

  • Python大好き(TypeScriptほぼ経験無し

  • 株式会社ユーザベースでデータサイエンティスト(自然言語処理、XP)

突然の「VS Code拡張を作りたい!」

  • それは2022年9月のこと

  • 作るために必要な 情報には見当 がついていた

  • 「うまくいけば作れるんじゃないか」と拡張開発に体当たり

自作した TOKIMEKI editing

https://marketplace.visualstudio.com/items?itemName=everlasting-diary.tokimeki-editing

砕けなかった🙌

環境情報

  • VS Code 1.74.3

  • Node.js v16.14.2

  • npm 8.5.0

    "@vscode/vsce": "^2.16.0",
    "generator-code": "^1.7.2",
    "yo": "^4.3.1"

経験のない拡張開発の旅路

  1. テキストをベースに写経 & 改造(メインパート)

  2. 拡張のE2Eテストを書く

  3. 拡張を公開

Part 1. テキストを写経 & 改造しよう

  1. テキストをベースに写経 & 改造 (メインパート)

  2. 拡張のE2Eテストを書く

  3. 拡張を公開

知っていたテキスト

拡張機能の基礎を学びたい人(beginner)向け

  • Hello World(簡単な拡張 を作って起動する)

  • コードレンズ のボタンから ドキュメントを編集 する拡張を作る

経験のない試みなので、ハードルを下げた(これでもできるか分からなかった)

時を戻そう(当時のnikkieに助言するなら)

  • beginner向けテキスト を改造する(変更なし)

  • 英語のドキュメントにあたったが、拡張開発までカバーした書籍(参考文献参照)より早道(日本語で多くの情報を得る

テキストを写経 & 改造

  1. 写経:Hello Worldを動かす

  2. 写経:ドキュメントを編集する拡張を動かす

  3. ドキュメントの編集を自分がやりたい編集の仕方に改造する

1-1 Hello World

  1. 写経:Hello Worldを動かす

  2. 写経:ドキュメントを編集する拡張を動かす

  3. ドキュメントの編集を自分がやりたい編集の仕方に改造する

Hello Worldを動かす

  • 開発環境構築

    • ツールのセットアップ

    • scaffold(拡張開発に必要なファイル一式の生成)

  • 開発中の拡張を動かす

インストールしたもの

  • Node.js のLTSバージョン

    • ハンズオンテキストでは16.13.0

  • VS Codeに ESLint拡張機能

    • 拡張開発中のバグ混入を防ぐ

事前準備 必要な開発環境を整えよう

npm install yo generator-code

yo:

Yeoman scaffold=足場(テンプレートに沿ったファイル群)を作ってくれるツール

generator-code:

YeomanのVS Code拡張向けテンプレート

ref: 事前準備 必要な開発環境を整えよう

yo code でscaffoldするんだYO

hello-vscode

hello-vscode/
├── .vscode/
├── src/
│   └── extension.ts
├── package.json
└── package-lock.json

上記は抜粋版。 Hello Worldを作成しよう に詳細な説明あります

Hello Worldを動かす

  • 開発環境構築✅

  • 開発中の拡張を動かす

Hello World拡張がscaffoldされている

  • yo code しただけ。 実装は不要

  • VS Codeで F5 で拡張をビルド(新しいVS CodeのWindow)

Hello World!(デモ)

  • コマンド パレット(F1)を開いて

  • 「Hello World」コマンドを呼び出す

Hello Worldを起動しよう

コマンド実行結果、右下から来るぞ!

../_images/01extension-hello-world.png

src/extension.ts

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
	let disposable = vscode.commands.registerCommand('hello-vscode.helloWorld', () => {
		vscode.window.showInformationMessage('Hello World from Hello VSCode!');
	});
export function deactivate() {}
  • showInformationMessage でVS CodeのWindow右下にメッセージが出た

Hello Worldを起動しよう

メッセージの内容を変えてみる

  • ソースを編集 したら Shift + Command + F5 でRestart

  • コマンドパレットから「Hello World」コマンドを呼び出すと、メッセージが変わっている!

  • 「拡張開発できている!!」🙌

Hello Worldではユーザが呼び出せる コマンド を追加した

  • package.json

  • src/extension.ts

ユーザが使えるコマンドの秘密は、この2つのファイル

package.json にコマンドのID 🏃‍♂️== skip

hello-vscode.helloWorld という識別子のコマンドを宣言

  "contributes": {
    "commands": [
      {
        "command": "hello-vscode.helloWorld",
        "title": "Hello World"
      }
    ]
  },

src/extension.ts にコマンドの実装 🏃‍♂️

commands.registerCommandhello-vscode.helloWorld コマンドを実装と紐付けた

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
	let disposable = vscode.commands.registerCommand('hello-vscode.helloWorld', () => {
		vscode.window.showInformationMessage('Hello World from Hello VSCode!');
	});
export function deactivate() {}

🥟Hello World 小まとめ

1-2 マークダウンを編集する拡張

  1. 写経:Hello Worldを動かす

  2. 写経:ドキュメントを編集する拡張を動かす

  3. ドキュメントの編集を自分がやりたい編集の仕方に改造する

やりたいことは、日付の挿入

  • マークダウンファイル

  • 見出しに日付を挿入 したい

before
## VSCodeConJP
after
## 2023/01/21 VSCodeConJP

日付挿入ロジック

  • 1つ以上の # を「同じ数の # とその日の日付」で 置き換え

  • ## VSCodeConJP

    • ##(空白) ➡️ ## 2023/01/21(空白)

- ## VSCodeConJP
+ ## 2023/01/21 VSCodeConJP

ハンズオンにおける実装:CodeLens(コードレンズ)

CodeLens - Show Actionable Context Information Within Source Code

ソースコード中に表示される、ユーザがアクションできるリンク

CodeLensを使った日付挿入(デモ)

../_images/01extension-add-date-codelens-example.png

CodeLensを使った日付挿入 🏃‍♂️

  • add date がCodeLens。ユーザはクリックできる

  • クリックすると日付挿入##(空白)## 2023/01/21(空白) に置き換え

CodeLensを使った日付挿入を実現する要素

  • CodeLens

  • コマンド

    • ユーザがCodeLensをクリックしたら、コマンドが呼び出され、日付挿入される

ファイルにCodeLensを用意するには

  • 今回はマークダウンファイル中にCodeLensを設定する

  • vscode.CodeLensProvider

    • 実装して独自の CodeLensProvider クラスを定義

CodeLensProvider で複数のCodeLensを作る

  • CodeLens1つ1つは「何行目の何文字目から何文字目まで」(range)に作られる

  • rangeを求めるのに正規表現を使う(後述)

マークダウンの見出しの行を 正規表現 で見つける

  • /^#+\s/g と一致するrange(すべて)にCodeLensを作成

  • 例:1行目が ## VSCodeConJP

    • ##(空白) にCodeLensを作る(add date というリンク)

コマンドを呼び出すようにCodeLensを作成

provideCodeLenses メソッド(CodeLensProvider クラス)
        codeLenses.push(
          new vscode.CodeLens(range, {
            title: "add date",
            tooltip: "add date",
            command: "markdown-date.addDate",
            arguments: [range],
          })
        );
return codeLenses;

markdown-date.addDate コマンド

commands.registerCommand("markdown-date.addDate", (range: Range) => {
  if (vscode.window.activeTextEditor) {
    // ## VSCodeConJP の場合、textが「## 」
    const text = vscode.window.activeTextEditor.document.getText(range);

    const today = dayjs().format("YYYY-MM-DD");

    vscode.window.activeTextEditor.edit((editBuilder) => {
      // ## VSCodeConJP の場合、「## 」を表すrangeを「## 2023/01/21 」で置き換える
      editBuilder.replace(range, text + today + " ");
    });
  }
});

この拡張が機能するのはマークダウンだけに設定 🏃‍♂️

  1. Markdown を開いたときに拡張機能が起動するようにする

  2. 拡張機能が起動したときに CodeLensProvider を起動する

ドキュメントを編集しよう

Markdown を開いたときに拡張機能が起動 🏃‍♂️

拡張機能が起動するタイミングを、対象の言語 ID のファイルを開いたときにまで遅らせる

  "activationEvents": [
    "onLanguage:markdown"
  ],

拡張機能が起動したときに CodeLensProvider を起動 🏃‍♂️

export function activate(context: ExtensionContext) {
  const codelensProvider = new CodelensProvider();

  let disposable = languages.registerCodeLensProvider(
    { language: "markdown" },
    codelensProvider
  );
  disposables.push(disposable);
}

🥟マークダウンを編集する拡張 小まとめ

  • 1行目の見出しに日付を挿入できるようにしたい

  • CodeLensと、CodeLensクリックでコマンド呼び出し

  • vscode.CodeLensProvider を実装(正規表現にマッチしたrangeに CodeLens を作る)

テキストの実装をもっと詳しく知りたい方向け 🏃‍♂️

IMO:VS Codeの操作、内部的にはすべてはコマンド 🏃‍♂️

  • Hello Worldコマンド(ユーザが コマンドパレット から呼び出せる)

  • CodeLensでマークダウン編集(コマンド を呼び出して編集を実現)

1-3 マークダウン編集ロジックを改造

  1. 写経:Hello Worldを動かす

  2. 写経:ドキュメントを編集する拡張を動かす

  3. ドキュメントの編集を自分がやりたい編集の仕方に改造する

実は 無限 に日付を挿入できる

  • ## VSCodeConJP

  • ## 2023/01/21 VSCodeConJP

    • ##(空白) の部分にCodeLensが作成される

  • ## 2023/01/21 2023/01/21 VSCodeConJP

改造案を着想

  • 見出しに限らず、特定の文字列の後ろにemojiを挿入できるのでは!?

  • 文字列「歩夢」の後ろに「🎀」を挿入(「歩夢」を「歩夢🎀」で置換)

  • 歩夢 ➡️ 歩夢🎀 ➡️ 歩夢🎀🎀 ➡️ 歩夢🎀🎀🎀 🤩

これを実装し、公開したのが

TOKIMEKI Editing🌈

公開までの手順は続くパートで

🌯Part 1. テキストを写経 & 改造しよう まとめ

  • vscodejp/handson-hello-vscode-extension beginner向けをベースに、マークダウンを編集する拡張を写経・改造

  • VS Codeの概念:「コマンド」「CodeLens」

    • CodeLensからコマンドを呼び出し、CodeLensが設定されている範囲を編集 できる

関連アウトプット 🏃‍♂️

Part 2. 拡張のE2Eテストを書こう

  1. テキストをベースに写経 & 改造(メインパート)

  2. 拡張のE2Eテストを書く

  3. 拡張を公開

テストを書きたい!

  • 改造がうまくいき、公開が見え てきた✨

  • IMO「自分が作ったもので、自分以外が使う可能性があるなら、テストを書きたい」

VS Codeを操作するE2Eテスト、どう書くんだ?

実はテストはscaffoldされてるYO

  • Yeomanにより src/test 以下にテストに使うファイルが生成されています

src/
├── test/
│   ├── suite/
│   │   ├── extension.test.ts
│   │   └── index.ts
│   └── runTest.ts
└── extension.ts

テスト実行(以下のどちらか)

Downloading VS Code [==============================] 100%

テストを繰り返し実行できるように設定

開発環境(macOS)で、テストの2回めが実行できなかったのに対処

src/test/runTest.ts
async function main() {
    // ... 省略 ...
    await runTests(
        {
            extensionDevelopmentPath,
            extensionTestsPath,
            launchArgs: ['--user-data-dir', `${tmpdir()}`]
        });
    // ... 省略 ...
}

マークダウンを編集する拡張のテスト(IMO)

  1. マークダウンファイルに対するCodeLensの 設定 (個数や行)

  2. ユーザがCodeLensをクリックしたのをエミュレートした コマンド実行

1のref: typescript-language-featuresのCodeLensのE2E

2-1 CodeLensの設定をテスト

  • 🅰️テストに使うマークダウンファイル(フィクスチャ

  • 🅱️テストコードでCodeLensを取得する

🅰️フィクスチャの設定

  1. フィクスチャを配置

  2. フィクスチャを使うように設定(src/test/runTest.ts

🅰️-1 フィクスチャを配置

.
├── src/
├── test-fixtures/
│   └── markdown/
│       └── example.md
├── package.json
└── package-lock.json

フィクスチャ

test-fixtures/markdown/example.md
# Test of tokimeki-editing

## テスト歩夢

歩夢の行にemojiを追加できる

この行には何もしません

🅰️-2 フィクスチャを使うように設定

src/test/runTest.ts
async function main() {
    // ... 省略 ...
    const testWorkspace = path.resolve(__dirname, '../../test-fixtures');
    await runTests(
        {
            extensionDevelopmentPath,
            extensionTestsPath,
            launchArgs: [testWorkspace, '--user-data-dir', `${tmpdir()}`]
        });
    // ... 省略 ...
}

テストコードでフィクスチャのマークダウンファイルを 開く

src/test/suite/extension.test.ts
const testFileLocation = '/markdown/example.md';

suite('Extension Test Suite', () => {
    let fileUri: vscode.Uri;
    let editor: vscode.TextEditor;

    setup(async () => {
        fileUri = vscode.Uri.file(vscode.workspace.workspaceFolders![0].uri.fsPath + testFileLocation);
        const document = await vscode.workspace.openTextDocument(fileUri);
        editor = await vscode.window.showTextDocument(document);
    });
});

🅱️テストコードで CodeLensを取得 する

vscode.commands.executeCommand<readonly vscode.CodeLens[]>('vscode.executeCodeLensProvider', fileUri, 100);

ref: typescript-language-featuresのCodeLensのE2E

CodeLensの設定をテスト

src/test/suite/extension.test.ts
const codeLenses = await vscode.commands.executeCommand<readonly vscode.CodeLens[]>('vscode.executeCodeLensProvider', fileUri, 100);

// フィクスチャのファイルでCodeLensはいくつ設定されるか - 2個
assert.strictEqual(codeLenses?.length, 2);
// フィクスチャのファイルでCodeLensは何行目に設定されるか - 2行目と4行目(歩夢がある行)
assert.strictEqual(codeLenses?.[0].range.start.line, 2);
assert.strictEqual(codeLenses?.[1].range.start.line, 4);

2-2 コマンド実行のテスト

ユーザがCodeLensをクリックしたのをエミュレートする意図

コマンド実行するテストコード

src/test/suite/extension.test.ts
test('Insert 🎀 after 歩夢', async () => {
    const COMMAND_NAME = "tokimeki-editing.addOshiEmoji";
    await sleep(1500);  // sleepはユーザ定義関数でブロッキングする(VS CodeのWindowのロード待ちの意図)
    // 4行目「歩夢の行にemojiを追加できる」の「歩夢」にCodeLensが設定されている。
    // このCodeLensがユーザにクリックされたときに呼び出されるのと同様のコマンド呼び出し(「歩夢」のrangeも渡す)
    await vscode.commands.executeCommand(COMMAND_NAME, new vscode.Range(new vscode.Position(4, 0), new vscode.Position(4, 2)));
    await sleep(500);

    const actual = editor.document.lineAt(4).text;
    assert.strictEqual(actual, '歩夢🎀の行にemojiを追加できる');
});

テストの実行時間が伸びたことでテストが落ちないようにする

src/test/suite/index.ts
export function run(): Promise<void> {
    const mocha = new Mocha({
        ui: 'tdd',
        color: true,
        timeout: 5000  // 2000msから伸ばした(60000msまで伸ばしてもよいかも)
    });

    // ... 省略 ...
}

Mochaのtimeout設定 を変更

🌯Part 2. 拡張のE2Eテストを書こう まとめ

  • マークダウンを編集できるCodeLensを提供する拡張について、E2Eテストを書いた

    • CodeLensの 設定 を検証

    • ユーザがCodeLensをクリックして呼び出される コマンド を実行して検証

  • runTestsの launchArgs ・Mochaの設定

関連アウトプット 🏃‍♂️

Part 3. 拡張を公開しよう

  1. テキストをベースに写経 & 改造(メインパート)

  2. 拡張のE2Eテストを書く

  3. 拡張を公開

Marketplace へ公開!

  1. ブラウザからアップロードして公開

  2. コマンドラインで操作して公開

ドキュメント Publishing Extensions を参照

3-1 ブラウザからアップロードして公開

  • Visual Studio Marketplace publisher management page

  • vsce コマンド

Visual Studio Marketplace publisher management page

Nameだけ入力すればpublisherは作れる

../_images/01extension-create-publisher.png

vsce コマンド

npm install @vscode/vsce

ref: ドキュメントの「vsce > Installation」

package.json を編集

{
  // nameはMarketplaceで一意になるように変えるのがオススメ(チュートリアルのままだとかぶってしまう)
  "name": "tokimeki-editing",
  // publisher management pageで作ったpublisherのIDを書く
  "publisher": "everlasting-diary",
  // ... 省略 ....
}

vsce package

  • 拡張のソースコードを vsix ファイルにまとめる

    • tokimeki-editing-0.0.1.vsix 🙌

  • Yeomanでscaffoldした README.md のままだとpackageでエラー。編集する(内容を削った)

ブラウザからアップロード

https://media.githubusercontent.com/media/microsoft/vscode-docs/5e40432c324328cdedd2cb6d62e8e3d5ff3a4c66/api/working-with-extensions/images/publishing-extension/add-extension.png

Marketplace に公開🙌

  • アップロード後、verifyされるまで少し待つ

    • パスしたらメールが来た📧 Extension publish on Visual Studio Marketplace

  • 公開後、拡張を右クリックして表示されるメニューから、非公開にも切り替えられる

3-2 コマンドラインで操作して公開

vsce package
vsce login <publisher name>
vsce publish

vsce login にはトークンが必要

$ vsce login everlasting-diary
Personal Access Token for publisher 'everlasting-diary':

Personal Access Token作成

  1. Azure DevOpsの組織(organization)を作る

  2. Azure DevOpsの組織でPersonal Access Tokenを作る

Azure DevOpsの 組織 を作る

Azure DevOpsの組織でPersonal Access Tokenを作る

../_images/01extension-azure-devops-create-token.png

User settings(右上のアイコン) > Personal access tokens

Personal Access Token作成時の必須項目

Name:

トークンの名前

Organization:

everlasting-diary

Scopes:

MarketplaceのManageだけ

ドキュメントの「Get a Personal Access Token」 に画像あり(次スライド)

https://media.githubusercontent.com/media/microsoft/vscode-docs/5e40432c324328cdedd2cb6d62e8e3d5ff3a4c66/api/working-with-extensions/images/publishing-extension/token2.png

控えたトークンで vsce login が通ります

vsce package
vsce login <publisher name>
vsce publish

🌯Part 3. 拡張を公開しよう まとめ

  • ブラウザからアップロードしてMarketplaceに公開:初めて の方🔰にオススメ

    • publisher management pageの操作 & vsce コマンド

  • コマンドラインで操作して公開: 慣れてきたら こちらを

    • Azure DevOpsでPersonal Access Tokenを作る

この先:楽にリリースするために 🏃‍♂️

  • GitHub ActionsなどCIツール上で、Marketplaceに公開するためのコマンドを実行

  • 👉 Continuous Integration

関連アウトプット 🏃‍♂️

まとめ🌯:Awakening Extension (拡張開発はじまるよ🔰)

  1. テキストをベースに写経 & 改造

  2. 拡張のE2Eテストを書く

  3. 拡張を公開

テキストをベースに写経 & 改造

  • Yeoman によるscaffold

  • CodeLens で編集

    • 正規表現にマッチする箇所に作成

    • コマンド 呼び出し

拡張のE2Eテストを書く

フィクスチャのファイルに対して以下を検証

  • CodeLensの 設定

  • CodeLensから実行される コマンド を直接実行した結果

拡張を公開

  • はじめは手動アップロード がオススメ

  • 慣れたら vsce コマンドを

ご清聴ありがとうございました!

Enjoy extension! 🎀🌈

References

vscodejp/handson-hello-vscode-extension 1/2

vscodejp/handson-hello-vscode-extension 2/2

VS Code ドキュメント(英語)

拡張開発まで扱った書籍

Supplement

補足

TypeScript

JavaScriptまわりの開発環境

  • npm install-g (グローバルにインストール)は意図して付けていません

  • Pythonで仮想環境を使った経験をベースにしていますが、言語が違うので適切なやり方ではないかもしれません

この先の拡張開発

  • 一案:ハンズオンテキストの本編(LSP編)へ

コントリビューションポイント

  • 「VS Codeを拡張できるAPIが用意された(限られた)箇所」 Visual Studio Code実践ガイド (第13章)

  • 拡張開発でどのAPIも利用できるわけではない(テストコードからCodeLens操作できないのもこの方針だからかも)

EOF