Sooua
登录
返回文章列表
OpenCode··5 分钟阅读

IDE 扩展开发

flowchart TB

目标:为 VS Code 等 IDE 开发 OpenCode 扩展


VS Code 扩展架构


开发环境准备

# 安装 Yeoman 和 VS Code 扩展生成器
npm install -g yo generator-code
 
# 生成扩展项目
yo code
 
# 选择: New Extension (TypeScript)
# 名称: opencode-helper

完整扩展示例

package.json

{
  "name": "opencode-helper",
  "displayName": "OpenCode Helper",
  "description": "OpenCode AI 编码助手",
  "version": "1.0.0",
  "engines": {
    "vscode": "^1.85.0"
  },
  "categories": ["Machine Learning", "Snippets"],
  "activationEvents": [
    "onCommand:opencode.explain",
    "onCommand:opencode.refactor"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "opencode.explain",
        "title": "Explain with OpenCode",
        "category": "OpenCode"
      },
      {
        "command": "opencode.refactor",
        "title": "Refactor with OpenCode",
        "category": "OpenCode"
      },
      {
        "command": "opencode.chat",
        "title": "Open Chat",
        "category": "OpenCode",
        "icon": "$(comment-discussion)"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "opencode.explain",
          "group": "9_cutcopypaste@5",
          "when": "editorHasSelection"
        }
      ]
    },
    "keybindings": [
      {
        "command": "opencode.chat",
        "key": "ctrl+shift+o",
        "mac": "cmd+shift+o"
      }
    ],
    "views": {
      "explorer": [
        {
          "id": "opencode.chatView",
          "name": "OpenCode Chat",
          "when": "opencode.enabled"
        }
      ]
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "tsc -p ./",
    "watch": "tsc -watch -p ./"
  },
  "devDependencies": {
    "@types/vscode": "^1.85.0",
    "typescript": "^5.3.0"
  }
}

扩展核心代码

// src/extension.ts
import * as vscode from 'vscode';
import { OpenCodeClient } from './client';
import { ChatPanel } from './chatPanel';
 
let client: OpenCodeClient;
 
export function activate(context: vscode.ExtensionContext) {
    // 初始化客户端
    const config = vscode.workspace.getConfiguration('opencode');
    const apiKey = config.get<string>('apiKey');
    
    if (!apiKey) {
        vscode.window.showWarningMessage(
            'OpenCode: 请配置 API 密钥',
            '打开设置'
        ).then(selection => {
            if (selection === '打开设置') {
                vscode.commands.executeCommand('workbench.action.openSettings', 'opencode.apiKey');
            }
        });
        return;
    }
    
    client = new OpenCodeClient({
        apiKey,
        baseURL: config.get<string>('baseURL') || 'https://api.opencode.ai'
    });
    
    // 注册命令
    context.subscriptions.push(
        vscode.commands.registerCommand('opencode.explain', explainCode),
        vscode.commands.registerCommand('opencode.refactor', refactorCode),
        vscode.commands.registerCommand('opencode.chat', () => ChatPanel.createOrShow(context.extensionUri, client))
    );
    
    // 注册装饰器
    registerDecorators(context);
}
 
async function explainCode() {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    const selection = editor.selection;
    const text = editor.document.getText(selection);
    
    if (!text) {
        vscode.window.showInformationMessage('请先选择一段代码');
        return;
    }
    
    // 显示进度
    await vscode.window.withProgress({
        location: vscode.ProgressLocation.Notification,
        title: 'OpenCode 正在分析代码...',
        cancellable: true
    }, async (progress, token) => {
        const explanation = await client.explain(text, token);
        
        // 在输出通道显示
        const channel = vscode.window.createOutputChannel('OpenCode');
        channel.appendLine('=== 代码解释 ===');
        channel.appendLine(explanation);
        channel.show();
    });
}
 
async function refactorCode() {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    const selection = editor.selection;
    const text = editor.document.getText(selection);
    
    const result = await client.refactor(text);
    
    // 显示 diff
    const doc = await vscode.workspace.openTextDocument({
        content: result,
        language: editor.document.languageId
    });
    await vscode.window.showTextDocument(doc, {
        viewColumn: vscode.ViewColumn.Beside
    });
}
 
function registerDecorators(context: vscode.ExtensionContext) {
    const decorationType = vscode.window.createTextEditorDecorationType({
        after: {
            contentText: ' 🤖',
            color: '#58a6ff'
        }
    });
    
    // 在修改过的行添加装饰
    vscode.workspace.onDidChangeTextDocument(event => {
        const editor = vscode.window.activeTextEditor;
        if (!editor || editor.document !== event.document) return;
        
        const decorations: vscode.DecorationOptions[] = [];
        for (const change of event.contentChanges) {
            const line = change.range.start.line;
            decorations.push({
                range: new vscode.Range(line, 0, line, 0)
            });
        }
        
        editor.setDecorations(decorationType, decorations);
    });
}
 
export function deactivate() {
    client?.dispose();
}

WebView 聊天面板

// src/chatPanel.ts
import * as vscode from 'vscode';
import { OpenCodeClient } from './client';
 
export class ChatPanel {
    public static currentPanel: ChatPanel | undefined;
    private readonly _panel: vscode.WebviewPanel;
    private _disposables: vscode.Disposable[] = [];
    
    private constructor(
        panel: vscode.WebviewPanel,
        private client: OpenCodeClient
    ) {
        this._panel = panel;
        this._panel.webview.html = this._getWebviewContent();
        
        // 处理消息
        this._panel.webview.onDidReceiveMessage(
            async message => {
                switch (message.command) {
                    case 'send':
                        const response = await this.client.send(message.text);
                        this._panel.webview.postMessage({
                            command: 'receive',
                            text: response
                        });
                        break;
                }
            },
            null,
            this._disposables
        );
        
        this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
    }
    
    static createOrShow(extensionUri: vscode.Uri, client: OpenCodeClient) {
        const column = vscode.window.activeTextEditor
            ? vscode.ViewColumn.Beside
            : undefined;
        
        if (ChatPanel.currentPanel) {
            ChatPanel.currentPanel._panel.reveal(column);
            return;
        }
        
        const panel = vscode.window.createWebviewPanel(
            'opencodeChat',
            'OpenCode Chat',
            column || vscode.ViewColumn.One,
            {
                enableScripts: true,
                retainContextWhenHidden: true
            }
        );
        
        ChatPanel.currentPanel = new ChatPanel(panel, client);
    }
    
    private _getWebviewContent(): string {
        return `<!DOCTYPE html>
        <html>
        <head>
            <style>
                body { font-family: sans-serif; padding: 10px; }
                #messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; }
                .user { color: #0066cc; margin: 5px 0; }
                .ai { color: #228822; margin: 5px 0; }
                #input { width: 80%; padding: 5px; }
                button { padding: 5px 15px; }
            </style>
        </head>
        <body>
            <div id="messages"></div>
            <input type="text" id="input" placeholder="输入消息...">
            <button onclick="send()">发送</button>
            
            <script>
                const vscode = acquireVsCodeApi();
                function send() {
                    const input = document.getElementById('input');
                    const text = input.value;
                    if (!text) return;
                    
                    addMessage('user', text);
                    vscode.postMessage({ command: 'send', text });
                    input.value = '';
                }
                
                function addMessage(role, text) {
                    const div = document.createElement('div');
                    div.className = role;
                    div.textContent = (role === 'user' ? '你: ' : '🤖: ') + text;
                    document.getElementById('messages').appendChild(div);
                }
                
                window.addEventListener('message', event => {
                    const message = event.data;
                    if (message.command === 'receive') {
                        addMessage('ai', message.text);
                    }
                });
            </script>
        </body>
        </html>`;
    }
    
    dispose() {
        ChatPanel.currentPanel = undefined;
        this._panel.dispose();
        while (this._disposables.length) {
            const x = this._disposables.pop();
            if (x) x.dispose();
        }
    }
}

其他 IDE

JetBrains 插件

// Kotlin 示例
class OpenCodeAction : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val editor = e.getRequiredData(CommonDataKeys.EDITOR)
        val selectedText = editor.selectionModel.selectedText
        
        // 调用 OpenCode API
        val result = OpenCodeClient.explain(selectedText)
        
        // 显示结果
        Messages.showMessageDialog(
            e.project,
            result,
            "OpenCode 解释",
            Messages.getInformationIcon()
        )
    }
}

Neovim 插件

-- opencode.nvim
local M = {}
 
function M.setup(opts)
    opts = opts or {}
    
    -- 注册命令
    vim.api.nvim_create_user_command('OpenCodeExplain', function()
        local buf = vim.api.nvim_get_current_buf()
        local start_pos = vim.api.nvim_buf_get_mark(buf, '<')
        local end_pos = vim.api.nvim_buf_get_mark(buf, '>')
        
        local lines = vim.api.nvim_buf_get_lines(buf, start_pos[1]-1, end_pos[1], false)
        local text = table.concat(lines, '\n')
        
        -- 调用 OpenCode
        vim.fn.jobstart({'curl', '-s', 'https://api.opencode.ai/explain', 
            '-d', vim.json.encode({code = text})}, {
            stdout_buffered = true,
            on_stdout = function(_, data)
                if data then
                    vim.notify(table.concat(data, '\n'), vim.log.levels.INFO)
                end
            end
        })
    end, {range = true})
end
 
return M

调试技巧

# VS Code 扩展调试
F5  # 启动调试
Ctrl+Shift+P  "Developer: Toggle Developer Tools"  # 查看 WebView 控制台

下一篇:22. 企业部署与治理

分享

评论

登录 后参与讨论。

加载中…

相关文章