mcpField
L
lit-and-vscode-extensions

lit-and-vscode-extensions

github.com/rodydavis開発6044 回インストール2 週間前に更新
リポジトリ

lit-and-vscode-extensions について

Lit の Web コンポーネントで VSCode 拡張機能を作る開発者向けチュートリアルです。Webview UI、メッセージ通信、TypeScript による拡張機能コマンドを扱います。

主な機能

  1. 01Lit と TypeScript のプロジェクト設定
  2. 02Lit Web コンポーネントの作成
  3. 03Webview メッセージによる通信
  4. 04拡張機能コマンドの登録

活用シーン

  1. 01Lit ベースの VSCode 拡張機能 UI の構築
  2. 02Webview コンポーネントと拡張機能ロジックの連携

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rodydavis/skills --skill lit-and-vscode-extensions

インストール後、Claude Code で/lit-and-vscode-extensionsを入力して読み込まれたか確認します。

詳細

Lit と VSCode 拡張機能

この記事では、Lit の Web コンポーネントをセットアップし、それを使って VSCode 拡張機能を作成する方法を説明します。

TLDR 最終的なソースコードはこちらで確認できます。

前提条件

  • Vscode
  • Node >= 16
  • Typescript

はじめに

まず、ターミナルでプロジェクトの場所に移動し、次のコマンドを実行します。

npm init @vitejs/app --template lit-ts

次に、プロジェクト名として lit-vscode-extension を入力します。続いてプロジェクトを vscode で開き、依存関係をインストールします。

cd lit-vscode-extension
npm i -D @types/node
code .

vite.config.ts を開き、次の内容に更新します。

import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
  base: "/lit-vscode-extension/",
  build: {
    outDir: "build",
    rollupOptions: {
      external: /^vscode/,
      input: {
        main: resolve(__dirname, "index.html"),
      },
      output: {
        entryFileNames: "[name].js",
      },
    },
  },
});

package.json を開き、次の内容に更新します。

{
  "name": "lit-vscode-extension",
  "description": "Lit VSCode Extension Example",
  "version": "0.0.1",
  "publisher": "rodydavis",
  "private": true,
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/rodydavis/lit-vscode-extension"
  },
  "engines": {
    "vscode": "^1.47.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [
    "onCommand:lit.start",
    "onCommand:lit.reset",
    "onWebviewPanel:lit"
  ],
  "main": "./build/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "lit.start",
        "title": "Open Plugin",
        "category": "lit"
      },
      {
        "command": "lit.reset",
        "title": "Reset",
        "category": "lit"
      }
    ]
  },
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "ext": "tsc src/extension.ts --outdir build --skipLibCheck --module commonjs",
    "compile": "npm run build && npm run ext",
    "serve": "vite preview"
  },
  "dependencies": {
    "lit": "^2.0.0-rc.2"
  },
  "devDependencies": {
    "@types/node": "^15.12.4",
    "@types/vscode": "^1.57.0",
    "typescript": "^4.2.3",
    "vite": "^2.3.5"
  }
}

package.json を更新したら、npm i を実行します。

テンプレート

index.html を開き、次の内容に更新します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Lit Example</title>
    <script type="module" src="/src/my-element.ts"></script>
  </head>
  <body>
    <my-element>
      <p>This is child content</p>
    </my-element>
  </body>
</html>

Web コンポーネント

src/my-element.ts を開き、次の内容に更新します。

import { html, css, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";

@customElement("my-element")
export class MyElement extends LitElement {
  static styles = css`
    :host {
      display: block;
      border: solid 1px gray;
      padding: 16px;
      max-width: 800px;
    }
  `;

  @property() name = "World";
  @state() count = 0;

  render() {
    return html`
      <h1>Hello, ${this.name}!</h1>
      <button @click=${() => this.modify(1)} part="button">
        Click Count: ${this.count}
      </button>
      <slot></slot>
    `;
  }

  modify(val: number) {
    this.count += val;
  }

  reset() {
    this.count = 0;
  }

  async firstUpdated() {
    window.addEventListener(
      "message",
      (e: any) => {
        const message = e.data;
        const { command } = message;
        if (command === "reset") {
          this.reset();
        }
      },
      false
    );
  }
}

ここでは、VSCode 拡張機能との通信に使うメッセージリスナー、カウントを更新するメソッド、およびレンダリングメソッドを追加するよう、サンプルを変更しています。

UI は iframe に読み込まれるため、VSCode は post メッセージを介してプラグインと通信します。

それでは、src/extension.ts に拡張機能のコードを書きましょう。まず、複数回参照するトップレベルの宣言を追加します。

import * as vscode from "vscode";

const WEB_DIR: string = "build";
const WEB_SCRIPT: string = "main.js";
const TITLE: string = "Lit Example";
const TAG: string = "my-element";


const possible = [
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "abcdefghijklmnopqrstuvwxyz",
  "0123456789",
].join("");

function getNonce() {
  let text = "";
  for (let i = 0; i < 32; i++) {
    const char = possible.charAt(Math.floor(Math.random() * possible.length));
    text += char;
  }
  return text;
}

また、読み込むスクリプトが渡したものと一致していることを確認するために使う getNonce メソッドも作成します。

次に、プラグインの UI を含む Panel を作成します。

class Panel {
  public static currentPanel: Panel | undefined;
  public static readonly viewType = "litExample";
  private _disposables: vscode.Disposable[] = [];

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;
    if (Panel.currentPanel) {
      Panel.currentPanel.panel.reveal(column);
      return;
    }
    const panel = vscode.window.createWebviewPanel(
      Panel.viewType,
      TITLE,
      column || vscode.ViewColumn.One,
      getWebviewOptions(extensionUri)
    );
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  private constructor(
    public readonly panel: vscode.WebviewPanel,
    public readonly extensionUri: vscode.Uri
  ) {
    this._update();
    this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
    this.panel.onDidChangeViewState(
      (_) => {
        if (this.panel.visible) {
          this._update();
        }
      },
      null,
      this._disposables
    );
    this.panel.webview.onDidReceiveMessage(
      (message) => {
        switch (message.command) {
          case "alert":
            vscode.window.showErrorMessage(message.text);
            return;
        }
      },
      null,
      this._disposables
    );
  }

  public sendMessage(command: string) {
    this.panel.webview.postMessage({ command: command });
  }

  public dispose() {
    Panel.currentPanel = undefined;
    this.panel.dispose();
    while (this._disposables.length) {
      const x = this._disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }

  private _update() {
    const webview = this.panel.webview;
    webview.html = this._getHtmlForWebview(webview);
  }

  private _getHtmlForWebview(webview: vscode.Webview) {
    const scriptPathOnDisk = vscode.Uri.joinPath(
      this.extensionUri,
      WEB_DIR,
      WEB_SCRIPT
    );
    const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
    const nonce = getNonce();

    const slot = "<p>This is child content</p>";

    return `<!DOCTYPE html>
			<html lang="en">
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>${TITLE}</title>
        </head>
        <body class="vscode-light">
          <${TAG} nonce="${nonce}" >
            ${slot}
          </${TAG}>
          <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
        </body>
			</html>`;
  }
}

index.html を使わずに HTML を再作成している点に注目してください。これにより、このコンポーネントをデプロイ済みの Web サイトでも、アプリのロジックコードを分けた拡張機能でも使えるようになります。

残りは、パネルの状態を管理し、使用後に破棄するための定型コードです。

次に、プラグインを読み込み、コマンドを監視するメソッドを追加します。

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand("lit.start", () => {
      Panel.createOrShow(context.extensionUri);
    })
  );

  context.subscriptions.push(
    vscode.commands.registerCommand("lit.reset", () => {
      if (Panel.currentPanel) {
        Panel.currentPanel.sendMessage("reset");
      }
    })
  );

  if (vscode.window.registerWebviewPanelSerializer) {
    vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
      async deserializeWebviewPanel(
        webviewPanel: vscode.WebviewPanel,
        state: any
      ) {
        console.log(`Received state: ${state}`);
        webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
        Panel.revive(webviewPanel, context.extensionUri);
      },
    });
  }
}

function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
  return {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
  };
}

ここでは拡張機能を読み込み、lit.reset コマンドが実行されたときにメッセージを渡しています。また、enableScripts が true に設定された getWebviewOptions のオプションも返しているため、挿入した JS を実行できます。

最終的なコードは次のようになります。

import * as vscode from "vscode";

const WEB_DIR: string = "build";
const WEB_SCRIPT: string = "main.js";
const TITLE: string = "Lit Example";
const TAG: string = "my-element";

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand("lit.start", () => {
      Panel.createOrShow(context.extensionUri);
    })
  );

  context.subscriptions.push(
    vscode.commands.registerCommand("lit.reset", () => {
      if (Panel.currentPanel) {
        Panel.currentPanel.sendMessage("reset");
      }
    })
  );

  if (vscode.window.registerWebviewPanelSerializer) {
    vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
      async deserializeWebviewPanel(
        webviewPanel: vscode.WebviewPanel,
        state: any
      ) {
        console.log(`Received state: ${state}`);
        webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
        Panel.revive(webviewPanel, context.extensionUri);
      },
    });
  }
}

function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
  return {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
  };
}

class Panel {
  public static currentPanel: Panel | undefined;
  public static readonly viewType = "litExample";
  private _disposables: vscode.Disposable[] = [];

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;
    if (Panel.currentPanel) {
      Panel.currentPanel.panel.reveal(column);
      return;
    }
    const panel = vscode.window.createWebviewPanel(
      Panel.viewType,
      TITLE,
      column || vscode.ViewColumn.One,
      getWebviewOptions(extensionUri)
    );
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  private constructor(
    public readonly panel: vscode.WebviewPanel,
    public readonly extensionUri: vscode.Uri
  ) {
    this._update();
    this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
    this.panel.onDidChangeViewState(
      (_) => {
        if (this.panel.visible) {
          this._update();
        }
      },
      null,
      this._disposables
    );
    this.panel.webview.onDidReceiveMessage(
      (message) => {
        switch (message.command) {
          case "alert":
            vscode.window.showErrorMessage(message.text);
            return;
        }
      },
      null,
      this._disposables
    );
  }

  public sendMessage(command: string) {
    this.panel.webview.postMessage({ command: command });
  }

  public dispose() {
    Panel.currentPanel = undefined;
    this.panel.dispose();
    while (this._disposables.length) {
      const x = this._disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }

  private _update() {
    const webview = this.panel.webview;
    webview.html = this._getHtmlForWebview(webview);
  }

  private _getHtmlForWebview(webview: vscode.Webview) {
    const scriptPathOnDisk = vscode.Uri.joinPath(
      this.extensionUri,
      WEB_DIR,
      WEB_SCRIPT
    );
    const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
    const nonce = getNonce();

    const slot = "<p>This is child content</p>";

    const htmlSource = `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>${TITLE}</title>
      </head>
      <body class="vscode-light">
        <${TAG} nonce="${nonce}" >
          ${slot}
        </${TAG}>
        <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
      </body>
    </html>`;

    return htmlSource;
  }
}

const possible = [
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "abcdefghijklmnopqrstuvwxyz",
  "0123456789",
].join("");

function getNonce() {
  let text = "";
  for (let i = 0; i < 32; i++) {
    const char = possible.charAt(Math.floor(Math.random() * possible.length));
    text += char;
  }
  return text;
}

実行

npm i を実行して、依存関係がインストールされていることを確認します。

拡張機能をビルドするには、npm run compile を実行します。

拡張機能を開いてデバッグするには、F5 を押します。

README の画像

拡張機能を開くコマンドを実行するには、lit: open plugin または lit: reset を実行します。

README の画像

拡張機能を開いた状態でデバッグするには、コマンドパレットから Developer: Open Webview Developer Tools を実行します。

README の画像

まとめ

vscode 拡張機能の構築について詳しく知りたい場合はこちらを、Lit についてはドキュメントのこちらを参照してください。

コメント