You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为VS Code状态栏项创建悬浮/上下文菜单(类似GitHub Copilot)

实现VS Code状态栏自定义交互菜单(仿GitHub Copilot)

要做跟GitHub Copilot一样的、紧贴状态栏的带复选框和操作按钮的菜单,VS Code原生API没有现成的组件可用,得结合DOM定位+Webview来搞定,以下是具体实现步骤:

实现思路

  1. 监听状态栏项的点击/悬浮事件,拿到它在窗口里的精确位置。
  2. 创建一个无边框、无标题的Webview面板,把它精准定位到状态栏项的正下方。
  3. 在Webview里自己写带复选框、按钮的UI,通过VS Code的消息机制和扩展主进程通信,处理设置切换、按钮点击这些交互。

代码实现

1. 扩展主文件(extension.ts)

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    // 创建状态栏项
    const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
    statusBarItem.text = '$(copilot) 我的扩展';
    statusBarItem.tooltip = '点击打开菜单';
    statusBarItem.show();
    context.subscriptions.push(statusBarItem);

    // 存一下Webview面板的实例,方便后续销毁
    let menuPanel: vscode.WebviewPanel | undefined;

    // 绑定状态栏项的点击事件
    statusBarItem.command = 'myExtension.openMenu';
    context.subscriptions.push(
        vscode.commands.registerCommand('myExtension.openMenu', async () => {
            await showCustomMenu(statusBarItem);
        })
    );

    // 显示自定义菜单的核心函数
    async function showCustomMenu(statusBarItem: vscode.StatusBarItem) {
        // 第一步:找到状态栏项的DOM元素,获取位置信息
        const statusBarElement = document.querySelector(`#status-bar [aria-label="${statusBarItem.tooltip}"]`);
        if (!statusBarElement) return;

        const rect = statusBarElement.getBoundingClientRect();
        const windowWidth = vscode.window.state.width;

        // 第二步:创建Webview面板
        if (menuPanel) {
            menuPanel.dispose();
        }

        menuPanel = vscode.window.createWebviewPanel(
            'customStatusMenu',
            '', // 空标题,我们要隐藏标题栏
            {
                viewColumn: vscode.ViewColumn.Active,
                preserveFocus: false,
                enableScripts: true,
                localResourceRoots: [context.extensionUri]
            },
            {
                disposeOnClose: true // 关闭面板时直接销毁
            }
        );

        // 设置Webview内容
        menuPanel.webview.html = getMenuWebviewContent(context, menuPanel.webview);

        // 监听Webview发来的消息,处理交互
        menuPanel.webview.onDidReceiveMessage(async (message) => {
            switch (message.type) {
                case 'toggleSetting':
                    // 处理设置切换:更新VS Code配置
                    const config = vscode.workspace.getConfiguration('myExtension');
                    await config.update(message.key, message.value, vscode.ConfigurationTarget.Global);
                    break;
                case 'executeAction':
                    // 处理按钮点击,比如示例里的Snooze
                    if (message.action === 'snooze') {
                        vscode.window.showInformationMessage('已启用Snooze');
                    }
                    menuPanel?.dispose();
                    break;
                case 'closeMenu':
                    menuPanel?.dispose();
                    break;
            }
        });

        // 延迟调整面板位置,确保DOM渲染完成
        setTimeout(() => {
            menuPanel?.webview.postMessage({
                type: 'positionMenu',
                rect: {
                    top: rect.bottom,
                    right: windowWidth - rect.right,
                    width: 280,
                    height: 180
                }
            });
        }, 100);
    }

    // 生成Webview的HTML内容
    function getMenuWebviewContent(context: vscode.ExtensionContext, webview: vscode.Webview) {
        const nonce = getNonce();
        return `
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <style>
                body {
                    margin: 0;
                    padding: 8px 0;
                    font-family: var(--vscode-font-family);
                    font-size: var(--vscode-font-size);
                    color: var(--vscode-foreground);
                    background-color: var(--vscode-menu-background);
                }
                .menu-item {
                    display: flex;
                    align-items: center;
                    padding: 6px 16px;
                    cursor: pointer;
                    user-select: none;
                }
                .menu-item:hover {
                    background-color: var(--vscode-menu-selectionBackground);
                }
                .menu-checkbox {
                    margin-right: 10px;
                    width: 16px;
                    height: 16px;
                }
                .menu-separator {
                    height: 1px;
                    background-color: var(--vscode-menu-separatorBackground);
                    margin: 4px 0;
                }
                .menu-button {
                    margin: 8px 16px;
                    padding: 6px 12px;
                    width: calc(100% - 32px);
                    background-color: var(--vscode-button-background);
                    color: var(--vscode-button-foreground);
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-family: inherit;
                    font-size: inherit;
                }
                .menu-button:hover {
                    background-color: var(--vscode-button-hoverBackground);
                }
            </style>
        </head>
        <body>
            <div class="menu-item" onclick="toggleSetting('enableFeature')">
                <input type="checkbox" class="menu-checkbox" id="enableFeature">
                <label for="enableFeature">启用核心功能</label>
            </div>
            <div class="menu-item" onclick="toggleSetting('autoSync')">
                <input type="checkbox" class="menu-checkbox" id="autoSync">
                <label for="autoSync">自动同步设置</label>
            </div>
            <div class="menu-separator"></div>
            <button class="menu-button" onclick="executeAction('snooze')">Snooze</button>

            <script nonce="${nonce}">
                const vscode = acquireVsCodeApi();

                // 初始化复选框状态:从扩展获取当前配置
                (async () => {
                    const config = await vscode.postMessage({ type: 'getConfig' });
                    document.getElementById('enableFeature').checked = config.enableFeature;
                    document.getElementById('autoSync').checked = config.autoSync;
                })();

                // 切换设置的函数
                function toggleSetting(key) {
                    const checkbox = document.getElementById(key);
                    const newValue = !checkbox.checked;
                    checkbox.checked = newValue;
                    vscode.postMessage({
                        type: 'toggleSetting',
                        key: key,
                        value: newValue
                    });
                }

                // 执行按钮操作的函数
                function executeAction(action) {
                    vscode.postMessage({
                        type: 'executeAction',
                        action: action
                    });
                }

                // 监听位置消息,调整菜单在窗口中的位置
                window.addEventListener('message', event => {
                    const message = event.data;
                    if (message.type === 'positionMenu') {
                        const rect = message.rect;
                        document.body.style.position = 'absolute';
                        document.body.style.top = \`\${rect.top}px\`;
                        document.body.style.right = \`\${rect.right}px\`;
                        document.body.style.width = \`\${rect.width}px\`;
                        document.body.style.height = \`\${rect.height}px\`;
                    }
                });

                // 点击菜单外部关闭(可选)
                document.addEventListener('click', event => {
                    if (!event.target.closest('.menu-item') && !event.target.closest('.menu-button')) {
                        vscode.postMessage({ type: 'closeMenu' });
                    }
                });
            </script>
        </body>
        </html>
        `;
    }

    // 生成安全用的nonce
    function getNonce() {
        let text = '';
        const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        for (let i = 0; i < 32; i++) {
            text += possible.charAt(Math.floor(Math.random() * possible.length));
        }
        return text;
    }
}

export function deactivate() {}

关键细节说明

  • 精准定位:通过document.querySelector找到状态栏项的DOM元素,获取它的位置后,把Webview面板直接放在它正下方,实现跟Copilot一样的紧贴效果。
  • 主题适配:用VS Code内置的CSS变量(比如--vscode-menu-background),菜单样式会自动跟随编辑器主题变化,不会违和。
  • 交互处理:Webview里的复选框、按钮通过postMessage把事件传给扩展主进程,主进程再处理配置更新、操作执行这些逻辑,完美支持交互。
  • 无标题栏:创建Webview时设置空标题,再通过CSS隐藏多余元素,看起来跟原生菜单一模一样。

关于你提到的旧方案

你之前找到的方案只能做静态弹窗,没法支持按钮、复选框这些交互元素,而这个Webview方案完全自定义UI,想加什么交互组件都可以,完全满足你的需求。

内容的提问来源于stack exchange,提问作者Abdelrahman Abdelaal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 02:54:53