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

如何在VSCode扩展中实现类似GitHub Copilot的内联表单UI?

实现VSCode扩展中类似GitHub Copilot的内联表单UI

我正在开发一款VSCode扩展,目标是给代码添加额外上下文信息。目前已经通过Code Actions实现了触发入口,但点击后弹出的是VSCode内置的快速输入框,无法满足需求——我需要一个类似GitHub Copilot的内联表单,包含两个输入框和一个提交按钮。


要实现这种内联式的自定义表单UI,你需要结合文本编辑器装饰(Text Editor Decorations)和Webview面板,而不是依赖内置的showInputBox。以下是完整的实现方案:

核心思路

  1. 用编辑器装饰在目标代码行附近标记表单的位置,预留显示空间
  2. 创建Webview面板并将其定位到目标行附近,模拟内联嵌入效果
  3. 在Webview中实现双输入框+提交按钮的表单,适配VSCode主题样式
  4. 通过Webview与扩展的消息通信,处理表单提交逻辑,完成代码插入后清理资源

修改后的完整代码

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(
        vscode.languages.registerCodeActionsProvider('typescript', new AddContextCodeActionProvider(), {
            providedCodeActionKinds: AddContextCodeActionProvider.providedCodeActionKinds
        })
    );

    context.subscriptions.push(
        vscode.commands.registerCommand('extension.addContext', (line: number) => {
            showInlineForm(line);
        })
    );
}

class AddContextCodeActionProvider implements vscode.CodeActionProvider {
    static readonly providedCodeActionKinds = [
        vscode.CodeActionKind.QuickFix
    ];

    provideCodeActions(document: vscode.TextDocument, range: vscode.Range): vscode.CodeAction[] {
        const addContextAction = this.createAddContextAction(document, range);
        return [addContextAction];
    }

    private createAddContextAction(document: vscode.TextDocument, range: vscode.Range): vscode.CodeAction {
        const action = new vscode.CodeAction('Add Context', vscode.CodeActionKind.QuickFix);
        action.command = { command: 'extension.addContext', title: 'Add Context', arguments: [range.start.line] };
        return action;
    }
}

function showInlineForm(targetLine: number) {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        return;
    }

    // 创建装饰标记表单位置并预留空间
    const decorationType = vscode.window.createTextEditorDecorationType({
        after: {
            contentText: ' ',
            margin: '0 0 0 1rem',
            height: '2.5rem'
        }
    });
    const range = new vscode.Range(targetLine, 0, targetLine, 0);
    editor.setDecorations(decorationType, [range]);

    // 创建Webview面板
    const panel = vscode.window.createWebviewPanel(
        'contextForm',
        '',
        {
            viewColumn: editor.viewColumn || vscode.ViewColumn.One,
            preserveFocus: true,
            enableFindWidget: false,
            retainContextWhenHidden: false
        },
        {
            enableScripts: true
        }
    );

    // 配置Webview样式与大小
    panel.title = '';
    panel.iconPath = undefined;
    panel.size = { width: 400, height: 120 };
    panel.webview.html = getFormHtml();

    // 监听Webview消息,处理表单提交/取消
    panel.webview.onDidReceiveMessage(
        async (message) => {
            if (message.type === 'submit') {
                const { context1, context2 } = message.data;
                if (context1 && context2) {
                    await editor.edit(editBuilder => {
                        const insertPosition = new vscode.Position(targetLine, 0);
                        editBuilder.insert(insertPosition, `// Context 1: ${context1}\n// Context 2: ${context2}\n`);
                    });
                }
                editor.setDecorations(decorationType, []);
                panel.dispose();
            } else if (message.type === 'cancel') {
                editor.setDecorations(decorationType, []);
                panel.dispose();
            }
        },
        null,
        context.subscriptions
    );

    // 面板关闭时清理装饰
    panel.onDidDispose(() => {
        editor.setDecorations(decorationType, []);
    });
}

// 生成适配VSCode主题的表单HTML
function getFormHtml() {
    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: 10px;
            padding: 0;
            font-family: var(--vscode-font-family);
            font-size: var(--vscode-font-size);
            background-color: var(--vscode-editor-background);
            color: var(--vscode-editor-foreground);
        }
        .form-group {
            margin-bottom: 10px;
        }
        label {
            display: block;
            margin-bottom: 4px;
            font-weight: 500;
        }
        input {
            width: 100%;
            padding: 6px 8px;
            border: 1px solid var(--vscode-input-border);
            border-radius: 3px;
            background-color: var(--vscode-input-background);
            color: var(--vscode-input-foreground);
        }
        input:focus {
            outline: none;
            border-color: var(--vscode-focusBorder);
        }
        .button-group {
            display: flex;
            gap: 8px;
            justify-content: flex-end;
            margin-top: 12px;
        }
        button {
            padding: 6px 12px;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-family: inherit;
            font-size: inherit;
        }
        .submit-btn {
            background-color: var(--vscode-button-background);
            color: var(--vscode-button-foreground);
        }
        .submit-btn:hover {
            background-color: var(--vscode-button-hoverBackground);
        }
        .cancel-btn {
            background-color: var(--vscode-button-secondaryBackground);
            color: var(--vscode-button-secondaryForeground);
        }
        .cancel-btn:hover {
            background-color: var(--vscode-button-secondaryHoverBackground);
        }
    </style>
</head>
<body>
    <div class="form-group">
        <label for="context1">上下文1</label>
        <input type="text" id="context1" placeholder="输入第一个上下文信息">
    </div>
    <div class="form-group">
        <label for="context2">上下文2</label>
        <input type="text" id="context2" placeholder="输入第二个上下文信息">
    </div>
    <div class="button-group">
        <button class="cancel-btn" onclick="handleCancel()">取消</button>
        <button class="submit-btn" onclick="handleSubmit()">提交</button>
    </div>

    <script>
        const vscode = acquireVsCodeApi();

        function handleSubmit() {
            const context1 = document.getElementById('context1').value.trim();
            const context2 = document.getElementById('context2').value.trim();
            vscode.postMessage({
                type: 'submit',
                data: { context1, context2 }
            });
        }

        function handleCancel() {
            vscode.postMessage({ type: 'cancel' });
        }

        // 支持回车提交表单
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                handleSubmit();
            }
        });
    </script>
</body>
</html>
`;
}

关键细节说明

  • 编辑器装饰:仅用于标记位置和预留显示空间,避免Webview遮挡代码
  • Webview主题适配:使用VSCode内置的CSS变量,保证表单样式与当前编辑器主题完全一致
  • 消息通信:通过postMessage和onDidReceiveMessage实现Webview与扩展的双向数据传递,处理提交和取消逻辑
  • 资源清理:无论表单提交还是关闭,都会自动清理编辑器装饰并销毁Webview面板

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 12:13:11