0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VSCodeの拡張機能を軽く自作してみたはなし

0
Last updated at Posted at 2026-06-08

つくれるとなんか楽できそうな気がするので、サンプルレベルでちょっとつくってみました。

つくったもの(仕様)

  • ターミナルで打ちがちなコマンド管理
    • コマンド一覧をVSCodeのタブの一つとして表示する
      • 情報はVSCode共通(ワークスペース外)に保存する
    • コマンドの追加ができる(+ボタンより)
    • コマンドのコピーができる(クリック)
    • コマンドの編集・削除ができる(ダブルクリック)

そもそも

使うだけなら作成途中に見つけた以下の記事で解説されている拡張機能を使えばよさそうです。
(参考にもさせていただきました)
https://zenn.dev/ikoamu/articles/f10441bab57efc

プロジェクト作成

途中警告出ますが動くのでそのままで...
TypeScriptかつesbuildを使う設定で準備しました。

# ツール準備
$ npm install -g yo generator-code
# プロジェクト作成
$ yo code

     _-----_     ╭──────────────────────────╮
    |       |    │   Welcome to the Visual  │
    |--(o)--|    │   Studio Code Extension  │
   `---------´   │        generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |     
   __'.___.'__   
 ´   `  |° ´ Y ` 

`list` prompt is deprecated. Use `select` prompt instead.
✔ What type of extension do you want to create? New Extension (TypeScript)
✔ What's the name of your extension? terminal-shortcut
✔ What's the identifier of your extension? terminal-shortcut
✔ What's the description of your extension? 
✔ Initialize a git repository? Yes
`list` prompt is deprecated. Use `select` prompt instead.
✔ Which bundler to use? esbuild
`list` prompt is deprecated. Use `select` prompt instead.
✔ Which package manager to use? pnpm

Writing in ...\ext-terminal-shortcut\terminal-shortcut...
   create terminal-shortcut\.vscode\extensions.json
   create terminal-shortcut\.vscode\launch.json
   create terminal-shortcut\.vscode\settings.json
   create terminal-shortcut\.vscode\tasks.json
   create terminal-shortcut\package.json
   create terminal-shortcut\tsconfig.json
   create terminal-shortcut\.vscodeignore
   create terminal-shortcut\esbuild.js
   create terminal-shortcut\vsc-extension-quickstart.md
   create terminal-shortcut\.gitignore
   create terminal-shortcut\README.md
   create terminal-shortcut\CHANGELOG.md
   create terminal-shortcut\src\extension.ts
   create terminal-shortcut\src\test\extension.test.ts
   create terminal-shortcut\.vscode-test.mjs
   create terminal-shortcut\eslint.config.mjs
   create terminal-shortcut\.npmrc

Changes to package.json were detected.

Running pnpm install for you to install the required dependencies.

 WARN  1 deprecated subdependencies found: glob@10.5.0
Packages: +358
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 384, reused 332, downloaded 26, added 358, done

devDependencies:
+ @types/mocha 10.0.10
+ @types/node 22.19.20 (25.9.2 is available)
+ @types/vscode 1.120.0
+ @vscode/test-cli 0.0.12
+ @vscode/test-electron 2.5.2
+ esbuild 0.27.7 (0.28.0 is available)
+ eslint 9.39.4 (10.4.1 is available)
+ npm-run-all 4.1.5
+ typescript 5.9.3 (6.0.3 is available)
+ typescript-eslint 8.60.1

╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮
│                                                                                            │
│   Ignored build scripts: esbuild@0.27.7.                                                   │
│   Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.   │
│                                                                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
Done in 19.3s using pnpm v10.32.1

Your extension terminal-shortcut has been created!

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

     code terminal-shortcut

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 'connor4312.esbuild-problem-matchers'.

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


`list` prompt is deprecated. Use `select` prompt instead.
✔ Do you want to open the new folder with Visual Studio Code? Skip

.vscode/launch.json も作成されているので、「F5」キーを押すと「デバッグ用VSCodeウィンドウ」が立ち上がります。
ホットリロードはしてくれない?みたいでソース編集する度に「止めてF5」していました。

製造

最終的にタブを「WebView」でつくりました。
公式でにゃんこが動くgitアニメの切り替えWebViewのサンプルがあります。こちらを参考にしていきます。
https://code.visualstudio.com/api/extension-guides/webview

VSCode のコマンドに追加

「Ctrl+Shift+P」で表示できるコマンド一覧に追加します。
package.json に以下の記載をすると、コマンド一覧に出てきます。(実行するコマンドが存在しないため、この時点では実行しようとするとエラーになります)

package.json
{
  // 省略: nameとか
  "activationEvents": [
    "onCommand:cterminal-shortcut.start" // 1.74より上のバージョンを対象とするなら書けとありました。contributes.commands[0].command と同じにします
  ],
  "contributes": {
    "commands": [
      {
        "command": "terminal-shortcut.start",
        "title": "Open terminal shorcuts",
        "category": "terminal-shortcut" // たぶん未指定でも動きますがサンプルで指定してたので...
      }
    ]
  },
  // 省略: dependenciesとか
}

WebView に表示するHTMLの準備

src/index.html をつくってみました。

src/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
  </head>
  <body>
    てきすと
  </body>
</html>

コマンド実行でタブを開く

src/extension.ts を以下の内容にします。

src/extension.ts
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('terminal-shortcut.start', () => terminalShortcutStart(context))
  );
}

// This method is called when your extension is deactivated
export function deactivate() {}

let panel: vscode.WebviewPanel | undefined; // タブ情報
function terminalShortcutStart(context: vscode.ExtensionContext) {
  // 既に開いていたらアクティブにするだけ
  if (panel) {
    panel.reveal(vscode.ViewColumn.One);
    return;
  }

  // 新しいタブを生成
  panel = vscode.window.createWebviewPanel(
    'terminalShortcut', // Identifies the type of the webview. Used internally
    'Terminal Shortcut', // Title of the panel displayed to the user
    vscode.ViewColumn.One, // Editor column to show the new webview panel in.
    { enableScripts: true } // Webview options. More on these later.
  );
  panel.onDidDispose(() => panel = undefined); // 破棄されたとき変数初期化

  // タブ内容を設定
  panel.webview.html = fs.readFileSync(`${__dirname}/../src/index.html`, { encoding: 'utf8' });
}

この状態で「F5」して、開いたウィンドウで「Ctrl+Shift+P」から Open terminal shorcuts を選ぶと新しいタブで内容が「テキスト」となります。

WebView で表示するHTMLに実装

CSSやJSを別ファイルにも出せますがちょっと面倒なので、まるっと直書きしました。
生JSつらみ。

最終的なHTMLは以下の内容です。
イマドキCSS Nestingがデフォルトサポートされているのでそれも利用しています。

vscode.postMessage でWebView作成元に通知できますが、逆にメッセージ受信はできない気がしたのでHTMLにあとで埋め込みます。
(オブジェクト vscode には setState/getStateもありますが、別インスタンスと状態を共有できませんでした。開発モードだからかも?)

ここまでで、保存もコピーもできませんがタブ上での追加・編集ができるようになります。

src/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <style>
      body { padding: 20px; }
      button { cursor: pointer; }
      ul {
        list-style: none;
        line-height: 1.5em;
        padding-inline-start:0;
      }
      li {
        cursor: pointer;
        padding: 2px 4px;
        &:hover {
          background-color: #eee;
        }
      }
      input[name="name"] {
        width: 200px;
      }
      input[name="cmd"] {
        width: calc(100% - 216px);
      }
      #copy-success {
        position: fixed;
        inset: 0;
        justify-content: center;
        align-items: center;
        background-color: rgba(0,0,0,.1);
        font-size: 3em;
        display: none;
        &.show {
          display: flex;
        }
      }
    </style>
  </head>
  <body>
    <div id="copy-success">コピーOK</div>
    <button id="add">+</button>
    <ul id="ul"></ul>
    <script type="text/javascript">
      // VSCode APIの準備、複数回呼べないらしいです。
      const vscode = acquireVsCodeApi();
      // 設定値。あとで置換します。
      const state = []; // --replace--
      const ul = document.getElementById('ul');
      const dispSuccess = document.getElementById('copy-success');

      const s = (str) => str.replaceAll('"', '&quot;'); // 最低限のサニタイザー
      // 一覧の更新処理
      function updateLis() {
        ul.innerHTML = state.map((item, i) => `<li data-idx="${i}" data-name="${s(item.name)}" data-cmd="${s(item.cmd)}">${s(item.name)}(${s(item.cmd)})</li>`).join('');
      }
      updateLis(); // 初回の一覧作成

      // 追加ボタンを押したときの処理
      document.getElementById('add').addEventListener('click', () => {
        state.push({ name: 'しんき', cmd: '' });
        updateLis();
      });

      // 一覧1件クリック: コマンドをコピー
      const onClickLi = (li) => {
        // クリップボードへのコピーをメイン側で行うために通知
        vscode.postMessage({ cmd: 'copy-cmd', target: li.dataset.cmd });
        // 表示の更新
        lastClick.target = null;
        dispSuccess.classList.add('show');
        setTimeout(() => dispSuccess.classList.remove('show'), 1000);
      }
      // 一覧1件ダブルクリック: 編集モード
      const onDblClickLi = (li) => {
        li.innerHTML = `<input type="text" name="name" value="${s(li.dataset.name)}"><input type="text" name="cmd" value="${s(li.dataset.cmd)}"><button>x</button>`;
      }

      const lastClick = { target: null, timer: undefined }; // 前回のクリック情報
      ul.addEventListener('click', (e) => {
        const target = e.target;

        // 編集モードのxボタンを押したときに削除してデータ更新
        if (target instanceof HTMLButtonElement) {
          state.splice(Number(target.closest('li').dataset.idx), 1);
          updateLis();
          vscode.postMessage({ cmd: 'update-list', state });
          return;
        }

        // シングルクリック判定
        if (!lastClick.target || lastClick.target != target) {
          if (lastClick.timer) clearTimeout(lastClick.timer);
          if ((target instanceof HTMLLIElement) == false) return;
          lastClick.target = target;
          lastClick.timer = setTimeout(() => onClickLi(target), 200);
          return;
        }
        // ダブルクリックの処理
        clearTimeout(lastClick.timer);
        lastClick.target = null;
        lastClick.timer = undefined;
        onDblClickLi(target);
      });
      // 編集モードは入力欄のEnterキー入力で保存
      ul.addEventListener('keydown', (e) => {
        if (e.key.toLowerCase() != 'enter') return;
        const li = e.target.closest('li');
        const name = li.children[0].value;
        const cmd = li.children[1].value;
        state[Number(li.dataset.idx)] = { name, cmd };
        updateLis();
        // データはメイン側で保存するので通知
        vscode.postMessage({ cmd: 'update-list', state });
      });
    </script>
  </body>
</html>

メイン側で通知を受ける

関数 terminalShortcutStart の内容を以下にします。
これで、保存やクリップボードへのコピーも動きます。

src/extension.ts
  if (panel) {
    panel.reveal(vscode.ViewColumn.One);
    return;
  }

  panel = vscode.window.createWebviewPanel(
    'terminalShortcut', // Identifies the type of the webview. Used internally
    'Terminal Shortcut', // Title of the panel displayed to the user
    vscode.ViewColumn.One, // Editor column to show the new webview panel in.
    { enableScripts: true } // Webview options. More on these later.
  );
  panel.onDidDispose(() => panel = undefined); // 破棄されたとき変数初期化
  // ↑ ここより上は元と同じ

  // globalState: ワークスペースを横断したデータ保存先
  // globalState から取得した一覧情報をJSONとしてHTMLに埋め込んでWebViewの内容とする
  const data = context.globalState.get('list') ?? [];
  panel.webview.html = fs.readFileSync(`${__dirname}/../src/index.html`, { encoding: 'utf8' })
    .replace('[]; // --replace--', JSON.stringify(data));

  // WebViewからイベントが届いたときの処理
  panel.webview.onDidReceiveMessage((message) => {
    // 一覧の更新: globalState を更新
    if (message.cmd === 'update-list') {
      context.globalState.update('list', message.state);
    }
    // コマンドのコピー: クリップボードにコピー
    else if (message.cmd === 'copy-cmd') {
      vscode.env.clipboard.writeText(message.target);
    }
  });

パッケージ化してインストールする

準備

なんか警告が出るので、

  • Readmeをある程度ちゃんと書く
  • LICENSEファイルをつくる

をあらかじめしておきます。

また、今回は「dist/extension.js」と「src/index.html」だけあれば動くので、.vscodeignoreに以下の2行を追加します。

!src/index.html
pnpm-lock.yaml

パッケージ化

以下のコマンドを実行しました。

$ npm install -g @vscode/vsce
$ vsce package
Executing prepublish script 'npm run vscode:prepublish'...
npm warn Unknown project config "enable-pre-post-scripts". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.

> terminal-shortcut@0.0.1 vscode:prepublish
> pnpm run package


> terminal-shortcut@0.0.1 package ext-terminal-shortcut
> pnpm run check-types && pnpm run lint && node esbuild.js --production


> terminal-shortcut@0.0.1 check-types ext-terminal-shortcut
> tsc --noEmit


> terminal-shortcut@0.0.1 lint ext-terminal-shortcut
> eslint src

[watch] build started
[watch] build finished
 WARNING  A 'repository' field is missing from the 'package.json' manifest file.
Use --allow-missing-repository to bypass.
Do you want to continue? [y/N] y
 INFO  Files included in the VSIX:
terminal-shortcut-0.0.1.vsix
├─ [Content_Types].xml 
├─ extension.vsixmanifest 
└─ extension/
   ├─ LICENSE.txt [1.04 KB]
   ├─ changelog.md [0.24 KB]
   ├─ package.json [1.45 KB]
   ├─ readme.md [0.74 KB]
   ├─ dist/
   │  └─ extension.js [1.32 KB]
   └─ src/
      └─ index.html [3.3 KB]

 DONE  Packaged: ext-terminal-shortcut\terminal-shortcut-0.0.1.vsix (8 files, 5.56 KB)

これで、ext-terminal-shortcut\terminal-shortcut-0.0.1.vsix をVSCodeの「拡張機能 > 左側に開くやつ > 右上「...」メニュー > VSIX からのインストール...」でインストールできます。

リポジトリ

8000年ぶりにGitHubにPUSHしました。
いろいろ荒いのでVSCodeのストアには上げてません。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?