如何为VS Code状态栏项创建悬浮/上下文菜单(类似GitHub Copilot)
实现VS Code状态栏自定义交互菜单(仿GitHub Copilot)
要做跟GitHub Copilot一样的、紧贴状态栏的带复选框和操作按钮的菜单,VS Code原生API没有现成的组件可用,得结合DOM定位+Webview来搞定,以下是具体实现步骤:
实现思路
- 监听状态栏项的点击/悬浮事件,拿到它在窗口里的精确位置。
- 创建一个无边框、无标题的Webview面板,把它精准定位到状态栏项的正下方。
- 在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
相关产品推荐
相关产品推荐

