From 196a5eba2a88bb89ee7e5de4f87cc8f1803971a2 Mon Sep 17 00:00:00 2001 From: ZLY Date: Fri, 25 Jul 2025 14:53:00 +0800 Subject: [PATCH] =?UTF-8?q?refactor(extension):=20=E9=87=8D=E6=9E=84=20AI?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscodeignore | 1 + src/extension.ts | 334 +++++++++++++++++++---------------------------- 2 files changed, 133 insertions(+), 202 deletions(-) diff --git a/.vscodeignore b/.vscodeignore index 7d3e5c7..df76e1f 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -3,6 +3,7 @@ src/** .gitignore .yarnrc +.idea vsc-extension-quickstart.md **/tsconfig.json **/eslint.config.mjs diff --git a/src/extension.ts b/src/extension.ts index 0c7b2e6..032acec 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,15 +3,14 @@ import * as path from 'path'; import * as diff from 'diff'; // 定义全局变量来存储面板状态 -let panel: vscode.WebviewPanel | undefined; let currentSessionHistory: { role: string; content: string }[] = []; // 存储工作区变更的文件,包含状态信息 -let workspaceChanges: { - [key: string]: { - original: string; - modified: string; - status: 'pending' | 'accepted' | 'rejected' - } +let workspaceChanges: { + [key: string]: { + original: string; + modified: string; + status: 'pending' | 'accepted' | 'rejected' + } } = {}; export function activate(context: vscode.ExtensionContext) { @@ -24,9 +23,20 @@ export function activate(context: vscode.ExtensionContext) { statusBarItem.show(); - // 注册打开 Webview 的命令 + // 注册侧边栏视图提供商 + const provider = new AISidebarViewProvider(context.extensionPath, context); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider( + 'ai-chat-sidebar-view', + provider, + { webviewOptions: { retainContextWhenHidden: true } } + ) + ); + + // 注册打开 Webview 的命令(保持向后兼容) const openWebviewCommand = vscode.commands.registerCommand("ai-chat.openWebview", () => { - openWebview(context.extensionPath, context.globalState, context); + // 聚焦到主侧边栏视图 + vscode.commands.executeCommand('workbench.action.focusSideBar'); }); // 注册右键菜单命令:添加选中内容到对话 @@ -46,147 +56,43 @@ export function activate(context: vscode.ExtensionContext) { const selectedText = editor.document.getText(selection); - // 检查 panel 是否存在 - if (panel && panel.webview) { - panel.webview.postMessage({ - command: 'addToInput', - role: 'user', - content: selectedText - }); - } else { - vscode.window.showWarningMessage("请先打开对话窗口"); - } - }); - - // 注册选择文件命令 - const selectFileCommand = vscode.commands.registerCommand('ai-chat.selectFileAsContext', async () => { - const uris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, // 单选 - title: "选择一个文件作为上下文" - }); - - if (uris && uris.length > 0) { - const selectedFileUri = uris[0]; - const fileContent = await vscode.workspace.fs.readFile(selectedFileUri); - const decodedContent = new TextDecoder("utf-8").decode(fileContent); - - // 发送文件内容到 Webview - if (panel && panel.webview) { - panel.webview.postMessage({ - command: 'setContextFile', - fileName: selectedFileUri.fsPath, - fileContent: decodedContent - }); - } - } - }); - - // 注册获取工作区文件列表的命令 - const getFileListCommand = vscode.commands.registerCommand('ai-chat.getFileList', async () => { - if (panel && panel.webview) { - try { - // 获取工作区根路径 - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - panel.webview.postMessage({ - command: 'fileList', - files: [], - error: '未找到工作区' - }); - return; - } - - const rootPath = workspaceFolders[0].uri; - - // 查找所有文件 - const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**'); - - // 转换为相对路径和基本信息 - const files = fileUris.map(uri => { - const relativePath = vscode.workspace.asRelativePath(uri); - const isDirectory = uri.path.endsWith('/'); - return { - path: uri.fsPath, - relativePath: relativePath, - name: path.basename(uri.fsPath), - isDirectory: isDirectory - }; - }); - - panel?.webview.postMessage({ - command: 'fileList', - files: files - }); - } catch (error: any) { - panel?.webview.postMessage({ - command: 'fileList', - files: [], - error: error.message - }); - } - } + // 发送选中的代码到侧边栏视图 + provider.sendSelectedCode(selectedText); }); // 添加到 subscriptions - context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand,selectFileCommand,getFileListCommand); + context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand); } -function openWebview( - extensionPath: string, - globalState: vscode.Memento, - context: vscode.ExtensionContext // 添加 context 参数 -) { - // 如果面板已经存在,直接显示它而不是创建新的 - if (panel) { - panel.reveal(vscode.ViewColumn.Beside); - // 发送当前状态到 WebView - if (panel.webview) { - panel.webview.postMessage({ - command: 'restoreState', - history: currentSessionHistory, - workspaceChanges: workspaceChanges - }); - } - return; - } - - panel = vscode.window.createWebviewPanel( - 'aiChatWebview', - 'AI Chat', - vscode.ViewColumn.Beside, - { - enableScripts: true, - localResourceRoots: [vscode.Uri.file(path.join(extensionPath, 'media', 'webview'))] - } - ); - - const styleUri = panel.webview.asWebviewUri( - vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'style.css')) - ); +class AISidebarViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = 'ai-chat-sidebar-view'; + + private _view?: vscode.WebviewView; + private _extensionPath: string; + private _context: vscode.ExtensionContext; - const scriptUri = panel.webview.asWebviewUri( - vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'script.js')) - ); - - const highlightScriptUri = panel.webview.asWebviewUri( - vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'highlight.min.js')) - ); - - const highlightStyleUri = panel.webview.asWebviewUri( - vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css')) - ); + constructor(extensionPath: string, context: vscode.ExtensionContext) { + this._extensionPath = extensionPath; + this._context = context; + } - const markedScriptUri = panel.webview.asWebviewUri( - vscode.Uri.file(path.join(context.extensionPath, 'media', 'webview', 'marked.min.js')) - ); + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + this._view = webviewView; + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview')) + ] + }; - panel.webview.html = getWebviewContent(styleUri, scriptUri,highlightScriptUri,highlightStyleUri,markedScriptUri); + webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); - panel.webview.onDidReceiveMessage( - async (message) => { + webviewView.webview.onDidReceiveMessage(async (message) => { switch (message.command) { case 'ask': const question = message.text; @@ -195,10 +101,10 @@ function openWebview( currentSessionHistory = [...history, { role: 'user', content: question }]; - panel?.webview.postMessage({ command: 'addMessage', role: 'user', content: question }); + this._postMessage({ command: 'addMessage', role: 'user', content: question }); try { - const response = await callQwenAPI(question, history, fileContent, context); + const response = await callQwenAPI(question, history, fileContent, this._context); console.log("response:", response); const aiMessage = response.choices[0]?.message?.content || '未获取到回复'; @@ -215,7 +121,7 @@ function openWebview( }; // 通知 WebView 更新工作区文件列表 - panel?.webview.postMessage({ + this._postMessage({ command: 'updateWorkspaceFiles', files: workspaceChanges }); @@ -224,68 +130,59 @@ function openWebview( // 更新当前会话历史 currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }]; - panel?.webview.postMessage({ + this._postMessage({ command: 'addMessage', role: 'ai', content: aiMessage, codeDiff: codeDiff // 发送代码差异信息 }); } catch (error: any) { - panel?.webview.postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message }); + this._postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message }); } - panel?.webview.postMessage({ command: 'hideLoading' }); + this._postMessage({ command: 'hideLoading' }); break; case 'selectContextFile': // 执行文件选择逻辑 - try { - const uris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - title: "选择一个文件作为上下文" - }); + const uris = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + title: "选择一个文件作为上下文" + }); - if (uris && uris.length > 0) { - const selectedFileUri = uris[0]; - const fileContent = await vscode.workspace.fs.readFile(selectedFileUri); - const decodedContent = new TextDecoder("utf-8").decode(fileContent); + if (uris && uris.length > 0) { + const selectedFileUri = uris[0]; + const fileContent = await vscode.workspace.fs.readFile(selectedFileUri); + const decodedContent = new TextDecoder("utf-8").decode(fileContent); - panel?.webview.postMessage({ - command: 'setContextFile', - fileName: selectedFileUri.fsPath, - fileContent: decodedContent - }); - } - } catch (error: any) { - // 忽略错误 - console.error(`文件操作错误: ${error.message}`); + this._postMessage({ + command: 'setContextFile', + fileName: selectedFileUri.fsPath, + fileContent: decodedContent + }); } break; case 'getFileList': // 执行获取文件列表逻辑 - try { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (workspaceFolders && workspaceFolders.length > 0) { - const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**'); - - const files = fileUris.map(uri => { - const relativePath = vscode.workspace.asRelativePath(uri); - return { - path: uri.fsPath, - relativePath: relativePath, - name: path.basename(uri.fsPath), - isDirectory: false // 简化处理,实际可以根据扩展名判断 - }; - }); + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**'); + + const files = fileUris.map(uri => { + const relativePath = vscode.workspace.asRelativePath(uri); + return { + path: uri.fsPath, + relativePath: relativePath, + name: path.basename(uri.fsPath), + isDirectory: false // 简化处理,实际可以根据扩展名判断 + }; + }); - panel?.webview.postMessage({ - command: 'fileList', - files: files - }); - } - } catch (error: any) { - // 忽略错误 + this._postMessage({ + command: 'fileList', + files: files + }); } break; @@ -296,7 +193,7 @@ function openWebview( const fileContent = await vscode.workspace.fs.readFile(fileUri); const decodedContent = new TextDecoder("utf-8").decode(fileContent); - panel?.webview.postMessage({ + this._postMessage({ command: 'setContextFile', fileName: fileUri.fsPath, fileContent: decodedContent @@ -354,7 +251,7 @@ function openWebview( } // 通知 WebView 更新工作区文件列表 - panel?.webview.postMessage({ + this._postMessage({ command: 'updateWorkspaceFiles', files: workspaceChanges }); @@ -375,7 +272,7 @@ function openWebview( } // 通知 WebView 更新工作区文件列表 - panel?.webview.postMessage({ + this._postMessage({ command: 'updateWorkspaceFiles', files: workspaceChanges }); @@ -418,7 +315,7 @@ function openWebview( break; case 'getCodeChanges': // 获取代码变更 - panel?.webview.postMessage({ + this._postMessage({ command: 'codeChanges', changes: workspaceChanges }); @@ -429,7 +326,7 @@ function openWebview( // 恢复显示历史消息 currentSessionHistory.forEach(msg => { - panel?.webview.postMessage({ + this._postMessage({ command: 'addMessage', role: msg.role, content: msg.content, @@ -439,23 +336,56 @@ function openWebview( // 恢复工作区变更 if (message.workspaceChanges) { - panel?.webview.postMessage({ + this._postMessage({ command: 'updateWorkspaceFiles', files: message.workspaceChanges }); } break; } - }, - undefined, - context.subscriptions - ); + }); + } + + public sendSelectedCode(code: string) { + if (this._view) { + this._view.show?.(true); + this._postMessage({ + command: 'addToInput', + role: 'user', + content: code + }); + } + } - // // 添加 dispose 监听器 - panel.onDidDispose(() => { - // 不再清空会话历史,让用户在重新打开面板时保留上下文 - panel = undefined; - }, null, context.subscriptions); + private _getHtmlForWebview(webview: vscode.Webview) { + const styleUri = webview.asWebviewUri( + vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'style.css')) + ); + + const scriptUri = webview.asWebviewUri( + vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'script.js')) + ); + + const highlightScriptUri = webview.asWebviewUri( + vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'highlight.min.js')) + ); + + const highlightStyleUri = webview.asWebviewUri( + vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css')) + ); + + const markedScriptUri = webview.asWebviewUri( + vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'marked.min.js')) + ); + + return getWebviewContent(styleUri, scriptUri, highlightScriptUri, highlightStyleUri, markedScriptUri); + } + + private _postMessage(message: any) { + if (this._view) { + this._view.webview.postMessage(message); + } + } } async function callQwenAPI(