3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

簡単なVSCode Extensionを作る

Last updated at Posted at 2023-02-20

VSCodeの拡張機能を作ってみる。

VSCodeの拡張機能を作ってみます、内容はほとんどハローワールドです。
今回の拡張機能を作るにあたり、以下の記事を参考にさせていただきました。

作るもの

  • cursorConvert
    • カーソル位置にある単語を ${} で囲む
  • selectConvert
    • 選択中の単語を ${} で囲む
  • snippets
    • 入力位置に ${$1} のsnippetを挿入する

snippetsのほうはsnippetsの設定ファイルを作ればvscode単体でもすぐ作れるものです。
しかしconvertのほうは私にはやり方がわかりませんでした、なので拡張機能を作って対応しようと思います。

雛形のダウンロード

$ npm install -g yo generator-code
$ yo code

先ほどの記事を参考にポチポチと質問に答えていきます、1年ほど経っていますが内容は変わっていませんでした。
package.managerの選択では npm yarn npnmが提示されました。

試しにnpnmを選択したところインストールが完了しました
しかし、ビルド時にエラーが出たのでやめました!

npmを使ってインストールします。

src/extension.ts を書いていく

extension.tsにはコマンドとして呼ばれたときの処理を書いていきます。

forTemplateLiteral.select

  //selectConvert
  let disposableSelect = vscode.commands.registerCommand(
    "forTemplateLiteral.select",
    async () => {
      const editor = vscode.window.activeTextEditor;
      if (editor) {
        const selections = editor.selections;
        for (const selection of selections) {
          const text = editor.document.getText(selection); //実行時の選択テキスト
          const newText = `\${${text}}`;
          // 元々選択してあるテキストを置き換える
          await editor.edit((editBuilder) => {
            editBuilder.replace(selection, newText);
          });
        }
        await vscode.commands.executeCommand("cursorRight");
      }
    }
  );
  context.subscriptions.push(disposableSelect);

こちらのConvertは、文字列を選択した状態からコマンドが実行されます。
このテキストの置き換えを実行後は、置き換えたテキスト全体が選択された状態になります。
少し使いづらいので、cursorRightを発生させ、置き換えたテキストの右端にカーソル位置を移動した状態で処理を終了します。
意図しない動作になる可能性もあるので、cursorRightは無いほうがいいかもしれません、

注意点としては、editor.edit() はawaitをつけて実行します。

forTemplateLiteral.cursor

  //cursorConvert
  let disposableCursor = vscode.commands.registerCommand(
    "forTemplateLiteral.cursor",
    async () => {
      //選択状態を解除する
      await vscode.commands.executeCommand("cancelSelection");
      //カーソル位置の単語を選択する
      await vscode.commands.executeCommand(
        "editor.action.addSelectionToNextFindMatch"
      );
      //選択した単語を変換する
      await vscode.commands.executeCommand("forTemplateLiteral.select");
      //
      //todo: restore original selections
      // 多分, 難しいのでやらない
      //
    }
  );
  context.subscriptions.push(disposableCursor);

カーソル位置の単語を選択するコマンドが用意されているので、それを実行してからforTemplateLiteral.selectを実行します。
実行前の選択状態を復元できるといいなと思いましたが、大変そうなのでやめました

forTemplateLiteral.snippets

  //snippets
  let disposableSnippets = vscode.commands.registerCommand(
    "forTemplateLiteral.snippets",
    () => {
      const editor = vscode.window.activeTextEditor;
      if (editor) {
        editor.insertSnippet(
          new vscode.SnippetString("${$1}"),
          editor.selection.active
        );
      }
    }
  );
  context.subscriptions.push(disposableSnippets);

こちらはvscode.SnippetStringを使って実行位置でsnippetsを起動します。
snippetsはvscodeのuser-snippetsと同様に操作できます、今回は$1の場所から始まり、入力後にタブキーを押すとコードの右端でsnippetが終了します。

package.jsonにてコマンド登録

package.jsonの抜粋

  "activationEvents": [],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "forTemplateLiteral.cursor",
        "title": "Template Literal",
        "category": "Cursor",
        "when": "editorTextFocus"
      },
      {
        "command": "forTemplateLiteral.select",
        "title": "Template Literal",
        "category": "Select",
        "when": "editorTextFocus"
      },
      {
        "command": "forTemplateLiteral.snippets",
        "title": "Template Literal",
        "category": "Snippets",
        "when": "editorTextFocus"
      }
    ]
  },

今回の場合、activationEventsは[]でいいようです。

titlecategoryを設定すると、VSCodeでは以下のように表示されます。

GreenShot20230220_150046-Window.png

ビルドとインストール

参考にした記事から何も変わっていないので割愛します。
npnmを使った場合 $ npx vsce packageでコケたのでnpmかyarnが簡単だと思います。

テスト

$ yo code ではテストに関するパッケージやファイルの設定も一通り済ませた状態になってます。

今回はあらかじめ作られているsrc/test/suite/extension.test.ts を変更して、今回作った拡張機能に関してのテストを実行します。

今回はじめてテストプログラムを書きました。

いきなりわからない(未解決)

VSCodeの左のデバッグタブから、対象を Extension Testsに変更し実行します。

しかしThere are issues with task "npm: watch-tests". See the output for more details.とポップアップが表示され実行できません。

Error: Invalid problemMatcher reference: $tsc-watch というエラーが出ています。

tasks.jsonを確認すると $tsc-watchを指定している部分でエラーメッセージの表示があります。

Unrecognized problem matcher. Is the extension that contributes this problem matcher installed?(1)

解決できずに諦めました、デバッグタブからのテスト実行はできていません。
終わりだ…。

npm testでテストを直接実行する

Run and Debug経由のテストは諦めましたが、私にはconsole.log()があるのでそれで頑張ってテストしたいと思います。

$ npm testでテストを実行します
breakpointを設定したり、watchしたりはできません。

extension.test.tsの内容

import * as assert from "assert";
import * as vscode from "vscode";

suite("Extension Test Suite", () => {
  // テストが開始されたことをユーザーに知らせる情報メッセージを表示する
  const publisher = "undefined_publisher"; //package.jsonのpublisher 無い場合は "undefined_publisher" 固定文字列が使用される
  const packageName = "fortemplateliteral"; //package.jsonのname nameは小文字限定,
  const extensionId = `${publisher}.${packageName}`; //publisher + "." + name 大文字小文字は区別しないようだ
  const commands = [
    "forTemplateLiteral.cursor",
    "forTemplateLiteral.select",
    "forTemplateLiteral.snippets",
  ];
  vscode.window.showInformationMessage("テストを開始します。");

  //拡張機能がインストールされているか
  test("Extension should be present", () => {
    assert.ok(vscode.extensions.getExtension(extensionId));
  });

  //拡張機能がアクティブにできるか
  test("Should activate the extension", async function () {
    await vscode.extensions.getExtension(extensionId)!.activate();
    assert.ok(true);
  });

  //拡張機能の各コマンドが実行できるか
  commands.forEach((command) => {
    test(`Should execute the command  ${command} `, async function () {
      const result = await vscode.commands.executeCommand(command);
      assert.strictEqual(result, undefined);
    });
  });

  //cursorが実行できるか
  test("convert command should convert cursor text", async function () {
    //新しいエディタを設定
    const newContent = "const name = Alice;";
    const newDocument = await vscode.workspace.openTextDocument({
      content: newContent,
      language: "javascript",
    });
    await vscode.window.showTextDocument(newDocument);
    //カーソル位置を作成して変更
    const selection = new vscode.Selection(0, 13, 0, 13);
    vscode.window.activeTextEditor!.selection = selection;
    //コマンド実行
    await vscode.commands.executeCommand("forTemplateLiteral.cursor");
    //コマンド実行後のテキストを取得
    const resultText = vscode.window.activeTextEditor?.document.getText();
    assert.strictEqual(resultText, "const name = ${Alice};");
  });

  //selectが1つの選択に対し実行できるか
  test("convert command should convert selected a word", async function () {
    //新しいエディタを設定
    const newContent = "const name = Alice;";
    const newDocument = await vscode.workspace.openTextDocument({
      content: newContent,
      language: "javascript",
    });
    await vscode.window.showTextDocument(newDocument);
    //選択状態を作成して変更
    const selection = new vscode.Selection(0, 13, 0, 18);
    vscode.window.activeTextEditor!.selection = selection;
    //コマンド実行
    await vscode.commands.executeCommand("forTemplateLiteral.select");
    //コマンド実行後のテキストを取得
    const resultText = vscode.window.activeTextEditor?.document.getText();
    assert.strictEqual(resultText, "const name = ${Alice};");
  });

  //selectが複数の選択に対し実行できるか
  test.only("convert command should convert selected multi word", async function () {
    //新しいエディタを設定
    const newContent = `const name = Alice;
const name1 = Alice;
const name12 = Alice;
const name13 = Alice;`;
    const newDocument = await vscode.workspace.openTextDocument({
      content: newContent,
      language: "javascript",
    });
    await vscode.window.showTextDocument(newDocument);
    //選択状態を作成して変更
    const selections = [];
    selections.push(new vscode.Selection(0, 13, 0, 18));
    selections.push(new vscode.Selection(2, 15, 2, 20));
    selections.push(new vscode.Selection(3, 15, 3, 20));
    vscode.window.activeTextEditor!.selections = selections;

    //console.log("選択中の単語");
    //for (const selection of vscode.window.activeTextEditor!.selections) {
    //  console.log(vscode.window.activeTextEditor!.document.getText(selection));
    //}

    //コマンド実行
    await vscode.commands.executeCommand("forTemplateLiteral.select");
    //コマンド実行後のテキストを取得
    const resultText = vscode.window.activeTextEditor?.document.getText();
    assert.strictEqual(
      resultText,
      `const name = \${Alice};
const name1 = Alice;
const name12 = \${Alice};
const name13 = \${Alice};`
    );
  });

  //snippetsが実行できるか
  test("snippets command should insert snippet text", async () => {
    //新しいエディタを設定
    const newContent = "const name = ";
    const newDocument = await vscode.workspace.openTextDocument({
      content: newContent,
      language: "javascript",
    });
    await vscode.window.showTextDocument(newDocument);
    //選択状態を作成する
    const selection = new vscode.Selection(0, 13, 0, 13);
    vscode.window.activeTextEditor!.selection = selection;
    //コマンド実行
    await vscode.commands.executeCommand("forTemplateLiteral.snippets");
    //エディタ上で入力を行う
    await vscode.commands.executeCommand("type", { text: "Alice" });
    //await vscode.commands.executeCommand("tab"); //これはタブキーが入力されるだけなので不可
    await vscode.commands.executeCommand("jumpToNextSnippetPlaceholder"); //次のpladeholderに移動で終点に移動
    await vscode.commands.executeCommand("type", { text: ";" });
    //実行後のテキストを取得
    const resultText = vscode.window.activeTextEditor?.document.getText();
    assert.strictEqual(resultText, "const name = ${Alice};");
  });

  //テストに対するmochaのtimeoutを変えてみるテスト;
  test.skip("check mocha timeout setting", async function () {
    const sleep = (msec: number) =>
      new Promise((resolve) => setTimeout(resolve, msec));
    this.timeout(11000);
    await sleep(10000);
  });
});

extensionId

  • package.jsonのnamepublisherが使用されます。
  • publisher.name がextensionIdになります
  • package.jsonにpublisherを設定していない場合, "undefined_publisher"が使われます
  • 今回は設定していないので "undefined_publisher.fortemplateliteral" がextensionIdになります。

その他

  • 各テストのskip等
    • test.skip()でスキップできます。
    • test.only()でそのテストのみを実行します。
  • mochaのテストにはデフォルトのtimeoutがある
    • デフォルト設定を変えてもいいですが、this.timeout(10000)等で変更できます
  • arrow functionで書くと
    • thisでmochaの設定を変更できません、上記のコードではアロー関数で書かれているものもあります。thisを使わなければ問題ないので、違いを意識するか書き方を統一するかはその人次第だと思います。

"snippets command should insert snippet text",

この部分でsnippetsコマンドを実行してテストしています。コード中のコメントにある通りですが、vscode.commands.executeCommand("tab")はタブキーの入力ではなくタブを挿入します。

snippetの移動はjumpToNextSnippetPlaceholderを使用します。
snippetの動作完了は acceptSnippetです。

実行してみる

$ npm testで実行します、"check mocha timeout setting"のテストのskipは外しておきます。

Extension Test Suite
✔ Extension should be present
✔ Should activate the extension
✔ Should execute the command forTemplateLiteral.cursor
✔ Should execute the command forTemplateLiteral.select
✔ Should execute the command forTemplateLiteral.snippets
✔ convert command should convert cursor text (142ms)
✔ convert command should convert selected a word
✔ convert command should convert selected multi word (41ms)
✔ snippets command should insert snippet text (116ms)
✔ check mocha timeout setting (10003ms)
10 passing (10s)

終わりに

変化の早いパッケージだと数ヶ月でもガラリと変わったりするので不安でしたが、$ yo codeは参考記事ほぼそのままでビルドまで終わりました。
VScodeのextension作成は資料は少ないかもしれませんが、参考にしたものがそのまま使えるので良いですね。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?