diff --git a/media/webview/index.html b/media/webview/index.html index d274c5a..3b18dfa 100644 --- a/media/webview/index.html +++ b/media/webview/index.html @@ -72,7 +72,7 @@
- +
diff --git a/media/webview/script.js b/media/webview/script.js index fd849e9..d609d29 100644 --- a/media/webview/script.js +++ b/media/webview/script.js @@ -1,5 +1,11 @@ +import {detectLanguage, getFileExtension} from "../../src/utils/common" + let selectedCodeForContext = ''; let contextFilePath = ''; // 存储当前上下文文件路径 +let streamingMessageElement = null; // 用于存储当前流式消息的DOM元素 +let streamingMessageContent = ''; // 用于存储当前流式消息的内容 +let currentReader = null; // 用于存储当前流的reader,以便可以取消 +let isRequestInProgress = false; // 标记是否有请求正在进行 const vscode = acquireVsCodeApi(); document.addEventListener('DOMContentLoaded', () => { @@ -7,18 +13,33 @@ document.addEventListener('DOMContentLoaded', () => { const userInput = document.getElementById('user-input'); const chatBox = document.getElementById('chat-box'); const loading = document.getElementById('loading'); - + const sendBtn = document.getElementById('send-btn'); // 获取发送按钮 + // 初始化隐藏工作区 const workspaceContainer = document.querySelector('.workspace-container'); if (workspaceContainer) { workspaceContainer.style.display = 'none'; } - chatForm.addEventListener('submit', (e) => { + chatForm.addEventListener('submit', async (e) => { e.preventDefault(); + + // 如果有请求正在进行,点击按钮将中断请求 + if (isRequestInProgress) { + // 发送取消请求的消息到扩展 + vscode.postMessage({ + command: 'cancelRequest' + }); + + resetSendButton(); + hideLoading(); + return; + } + const text = userInput.value.trim(); if (!text) return; + // 发送消息并禁用按钮 vscode.postMessage({ command: 'ask', text, @@ -29,8 +50,36 @@ document.addEventListener('DOMContentLoaded', () => { // addMessage('user', text); showLoading(); userInput.value = ''; + + // 切换按钮为停止按钮 + isRequestInProgress = true; + if (sendBtn) { + sendBtn.textContent = '停止'; + sendBtn.classList.add('stop-button'); + } }); + // 重置发送按钮为初始状态 + function resetSendButton() { + isRequestInProgress = false; + const sendBtn = document.getElementById('send-btn'); + if (sendBtn) { + sendBtn.textContent = '发送'; + sendBtn.classList.remove('stop-button'); + } + } + + // 显示加载状态 + function showLoading() { + loading.classList.remove('hidden'); + chatBox.scrollTop = chatBox.scrollHeight; + } + + // 隐藏加载状态 + function hideLoading() { + loading.classList.add('hidden'); + } + // 判断内容是否为 Markdown(简单判断) function isMarkdown(content) { if (!content || content.trim() === '') return false; @@ -49,7 +98,7 @@ document.addEventListener('DOMContentLoaded', () => { } // 处理添加消息 - function addMessage(role, content, codeDiff) { + function addMessage(role, content, codeDiffs) { const msgDiv = document.createElement('div'); msgDiv.className = `message ${role}`; @@ -64,24 +113,29 @@ document.addEventListener('DOMContentLoaded', () => { msgDiv.appendChild(div); } - // 如果有代码差异,添加查看差异按钮 - if (codeDiff) { - const diffButton = document.createElement('button'); - diffButton.className = 'diff-button'; - diffButton.textContent = '查看代码差异'; - diffButton.onclick = () => showCodeDiff(codeDiff); - msgDiv.appendChild(diffButton); - } - - // 如果是AI消息且包含代码块,添加"创建文件"按钮 + // 如果是AI消息且包含代码块,为每个代码块添加按钮 if (role === 'ai') { const codeBlocks = msgDiv.querySelectorAll('pre code'); codeBlocks.forEach((block, index) => { + // 为每个代码块添加"创建文件"按钮 const createFileButton = document.createElement('button'); createFileButton.className = 'create-file-button'; createFileButton.textContent = `生成代码文件`; createFileButton.onclick = () => createNewFileFromCode(block.textContent, index); - msgDiv.appendChild(createFileButton); + + // 将按钮插入到代码块后面 + block.parentNode.after(createFileButton); + + // 如果有代码差异,也为每个代码块添加查看差异按钮 + // codeDiffs是一个数组,每个元素对应一个代码块的差异信息 + if (codeDiffs && codeDiffs[index]) { + const diffButton = document.createElement('button'); + diffButton.className = 'diff-button'; + diffButton.textContent = `查看代码差异`; + diffButton.onclick = () => showCodeDiff(codeDiffs[index]); + // 将差异按钮插入到创建文件按钮后面 + createFileButton.after(diffButton); + } }); } @@ -97,6 +151,147 @@ document.addEventListener('DOMContentLoaded', () => { // 滚动到底部 chatBox.scrollTop = chatBox.scrollHeight; + + return msgDiv; // 返回创建的消息元素 + } + + // 开始流式传输 + function startStreaming() { + // 创建一个新的AI消息元素 + streamingMessageElement = document.createElement('div'); + streamingMessageElement.className = 'message ai streaming'; + + // 添加流式消息内容容器 + const contentDiv = document.createElement('div'); + contentDiv.className = 'streaming-content'; + streamingMessageElement.appendChild(contentDiv); + + // 添加光标元素 + const cursor = document.createElement('span'); + cursor.className = 'streaming-cursor'; + cursor.textContent = '▋'; + streamingMessageElement.appendChild(cursor); + + chatBox.appendChild(streamingMessageElement); + streamingMessageContent = ''; + + // 滚动到底部 + chatBox.scrollTop = chatBox.scrollHeight; + } + + // 处理流式数据 + function handleStreamData(data) { + if (!streamingMessageElement) return; + + let processedData = data; + if (data.startsWith('data:')) { + // 移除data:前缀 + processedData = data.replace(/^data:/, ''); + // 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的 + processedData = processedData.replace(/^(\n{2,})/, (match) => { + // 删除所有连续的换行符 + return ''; + }); + } + + // 更新内容 + streamingMessageContent += processedData; + + // 更新显示 + const contentDiv = streamingMessageElement.querySelector('.streaming-content'); + if (contentDiv) { + // 如果是Markdown内容,我们需要特殊处理 + if (isMarkdown(streamingMessageContent)) { + contentDiv.innerHTML = marked.parse(streamingMessageContent); + } else { + contentDiv.textContent = streamingMessageContent; + } + + // 高亮代码块 + const codeBlocks = contentDiv.querySelectorAll('pre code'); + codeBlocks.forEach(block => { + if (hljs && !block.dataset.highlighted) { + hljs.highlightElement(block); + block.dataset.highlighted = 'true'; + } + }); + } + + // 滚动到底部 + chatBox.scrollTop = chatBox.scrollHeight; + } + + // 结束流式传输 + function endStreaming(content, codeDiffs = null) { + if (streamingMessageElement) { + // 移除流式传输类和光标 + streamingMessageElement.classList.remove('streaming'); + const cursor = streamingMessageElement.querySelector('.streaming-cursor'); + if (cursor) { + cursor.remove(); + } + + // 如果是AI消息且包含代码块,为每个代码块添加按钮 + if (streamingMessageElement.classList.contains('ai')) { + const codeBlocks = streamingMessageElement.querySelectorAll('pre code'); + codeBlocks.forEach((block, index) => { + // 为每个代码块添加"创建文件"按钮 + const createFileButton = document.createElement('button'); + createFileButton.className = 'create-file-button'; + createFileButton.textContent = `生成代码文件`; + createFileButton.onclick = () => createNewFileFromCode(block.textContent, index); + + // 将按钮插入到代码块后面 + block.parentNode.after(createFileButton); + + // 如果有代码差异,也为每个代码块添加查看差异按钮 + // codeDiffs是一个数组,每个元素对应一个代码块的差异信息 + if (codeDiffs && codeDiffs[index]) { + const diffButton = document.createElement('button'); + diffButton.className = 'diff-button'; + diffButton.textContent = `查看代码差异`; + diffButton.onclick = () => showCodeDiff(codeDiffs[index]); + // 将差异按钮插入到创建文件按钮后面 + createFileButton.after(diffButton); + } + }); + } + + // 高亮代码块 + const codeBlocks = streamingMessageElement.querySelectorAll('pre code'); + codeBlocks.forEach(block => { + if (hljs) { + hljs.highlightElement(block); + } + }); + + streamingMessageElement = null; + streamingMessageContent = ''; + } + + // 保存消息到历史记录 + currentSessionHistory.push({role: 'assistant', content: content}); + + hideLoading(); + } + + // 处理请求取消 + function handleRequestCancelled() { + // 重置发送按钮 + resetSendButton(); + + // 如果有正在流式传输的消息,结束它 + if (streamingMessageElement) { + streamingMessageElement.classList.remove('streaming'); + const cursor = streamingMessageElement.querySelector('.streaming-cursor'); + if (cursor) { + cursor.remove(); + } + streamingMessageElement = null; + streamingMessageContent = ''; + } + + hideLoading(); } // 显示代码差异 @@ -111,16 +306,16 @@ document.addEventListener('DOMContentLoaded', () => { codeDiff.added.forEach((line, index) => { const diffRow = document.createElement('div'); diffRow.className = 'diff-row'; - + // 修复行号显示问题 const lineNumber = document.createElement('div'); lineNumber.className = 'diff-line-number'; lineNumber.textContent = index + 1; // 使用索引+1作为行号 - + const diffLine = document.createElement('div'); diffLine.className = 'diff-line diff-added'; diffLine.textContent = `+ ${line}`; - + diffRow.appendChild(lineNumber); diffRow.appendChild(diffLine); diffContent.appendChild(diffRow); @@ -132,16 +327,16 @@ document.addEventListener('DOMContentLoaded', () => { codeDiff.removed.forEach((line, index) => { const diffRow = document.createElement('div'); diffRow.className = 'diff-row'; - + // 修复行号显示问题 const lineNumber = document.createElement('div'); lineNumber.className = 'diff-line-number'; lineNumber.textContent = index + 1; // 使用索引+1作为行号 - + const diffLine = document.createElement('div'); diffLine.className = 'diff-line diff-removed'; diffLine.textContent = `- ${line}`; - + diffRow.appendChild(lineNumber); diffRow.appendChild(diffLine); diffContent.appendChild(diffRow); @@ -164,7 +359,7 @@ document.addEventListener('DOMContentLoaded', () => { filePath: contextFilePath, modifiedContent: modifiedCode }); - + // 关闭模态框 document.getElementById('diff-modal').classList.add('hidden'); } @@ -178,25 +373,17 @@ document.addEventListener('DOMContentLoaded', () => { command: 'rejectChanges', filePath: contextFilePath }); - + // 关闭模态框 document.getElementById('diff-modal').classList.add('hidden'); } } - function showLoading() { - loading.classList.remove('hidden'); - } - - function hideLoading() { - loading.classList.add('hidden'); - } - // 更新工作区文件列表 function updateWorkspaceFiles(files) { const workspaceContainer = document.querySelector('.workspace-container'); const workspaceFilesContainer = document.getElementById('workspace-files'); - + // 如果没有文件变更,则隐藏工作区 if (!files || Object.keys(files).length === 0) { workspaceContainer.style.display = 'none'; @@ -206,19 +393,19 @@ document.addEventListener('DOMContentLoaded', () => { // 显示工作区 workspaceContainer.style.display = 'block'; workspaceFilesContainer.innerHTML = ''; - + Object.keys(files).forEach(filePath => { const fileChange = files[filePath]; const fileItem = document.createElement('div'); fileItem.className = 'workspace-file-item'; - + // 获取文件名 const fileName = filePath.split(/[\/\\]/).pop(); - + // 根据状态设置不同的显示样式 let statusText = ''; let statusClass = ''; - + switch (fileChange.status) { case 'accepted': statusText = '✓ 已接受'; @@ -232,13 +419,13 @@ document.addEventListener('DOMContentLoaded', () => { statusText = '待处理'; statusClass = 'status-pending'; } - + fileItem.innerHTML = ` 📄 ${fileName} ${statusText} `; - + fileItem.addEventListener('click', () => { // 发送消息到插件以打开文件 vscode.postMessage({ @@ -246,7 +433,7 @@ document.addEventListener('DOMContentLoaded', () => { filePath: filePath }); }); - + workspaceFilesContainer.appendChild(fileItem); }); } @@ -255,7 +442,19 @@ document.addEventListener('DOMContentLoaded', () => { const message = event.data; console.log(message) if (message.command === 'addMessage') { - addMessage(message.role, message.content,message.codeDiff); + addMessage(message.role, message.content, message.codeDiff); + } else if (message.command === 'startStream') { + // 开始流式传输 + startStreaming(); + } else if (message.command === 'streamData') { + // 处理流式数据 + handleStreamData(message.data); + } else if (message.command === 'endStream') { + // 结束流式传输 + endStreaming(message.content, message.codeDiffs); + } else if (message.command === 'requestCancelled') { + // 请求被取消 + handleRequestCancelled(); } else if (message.command === 'hideLoading') { hideLoading(); } else if (message.command === 'addToInput') { @@ -392,6 +591,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('diff-modal').classList.add('hidden'); } }); + // 从代码创建新文件 function createNewFileFromCode(codeContent, index) { // 简单的语言检测 @@ -407,41 +607,4 @@ document.addEventListener('DOMContentLoaded', () => { language: language }); } - - // 简单的语言检测 - function detectLanguage(code) { - // 可以根据代码特征进行简单判断 - if (code.includes('import React') || code.includes('from react')) { - return 'javascript'; // React代码 - } else if (code.includes('public class') || code.includes('private static')) { - return 'java'; - } else if (code.includes('def ') && code.includes(':')) { - return 'python'; - } else if (code.includes('function ') || code.includes('const ') || code.includes('let ')) { - return 'javascript'; - } else if (code.includes('interface ') && code.includes('export ')) { - return 'typescript'; - } else if (code.includes(' { + this._messageHandler.handleMessage(message); + }); + } + + public sendSelectedCode(code: string) { + if (this._view) { + this._view.show?.(true); + this._postMessage({ + command: 'addToInput', + role: 'user', + content: code + }); + } + } + + 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); + } + + public _postMessage(message: any) { + if (this._view) { + this._view.webview.postMessage(message); + } + } +} \ No newline at end of file diff --git a/src/MessageHandler.ts b/src/MessageHandler.ts new file mode 100644 index 0000000..3b0c55c --- /dev/null +++ b/src/MessageHandler.ts @@ -0,0 +1,489 @@ +import vscode from "vscode"; +import path from "path"; +import {callQwenAPI} from "./utils/modelApi"; +import {generateCodeDiff, getFileExtension} from "./utils/common"; + +// 定义全局变量来存储面板状态 +let currentSessionHistory: { role: string; content: string }[] = []; +// 存储工作区变更的文件,包含状态信息 +let workspaceChanges: { + [key: string]: { + original: string; + modified: string; + status: 'pending' | 'accepted' | 'rejected' + } +} = {}; + +export class MessageHandler { + private provider: any; // 这里应该使用AISidebarViewProvider类型,但由于循环依赖问题,暂时使用any + private context: vscode.ExtensionContext; + private currentReader: ReadableStreamDefaultReader | null = null; // 添加当前reader引用 + + constructor(provider: any, context: vscode.ExtensionContext) { + this.provider = provider; + this.context = context; + this.currentReader = null; + } + + public async handleMessage(message: any) { + switch (message.command) { + case 'ask': + await this.handleAsk(message); + break; + case 'cancelRequest': + await this.handleCancelRequest(); + break; + case 'selectContextFile': + await this.handleSelectContextFile(); + break; + case 'getFileList': + await this.handleGetFileList(); + break; + case 'selectFileByPath': + await this.handleSelectFileByPath(message); + break; + case 'showCodeDiffInEditor': + await this.handleShowCodeDiffInEditor(message); + break; + case 'acceptChanges': + await this.handleAcceptChanges(message); + break; + case 'rejectChanges': + await this.handleRejectChanges(message); + break; + case 'openWorkspaceFile': + await this.handleOpenWorkspaceFile(message); + break; + case 'getCodeChanges': + this.handleGetCodeChanges(); + break; + case 'createNewFile': + await this.handleCreateNewFile(message); + break; + case 'restoreState': + this.handleRestoreState(message); + break; + } + } + + private async handleAsk(message: any) { + const question = message.text; + const fileContent = message.fileContent; + const fileName = message.fileContentPath ? path.basename(message.fileContentPath) : "current_file.txt"; + const history = currentSessionHistory; + + currentSessionHistory = [...history, {role: 'user', content: question}]; + + this.provider._postMessage({command: 'addMessage', role: 'user', content: question}); + + try { + const response = await callQwenAPI(question, history, fileContent, this.context, fileName); + + // 发送开始流式传输的消息 + this.provider._postMessage({command: 'startStream', role: 'assistant'}); + + // 处理流式响应 + const reader = response.body?.getReader(); + this.currentReader = reader; // 保存reader引用 + const decoder = new TextDecoder('utf-8'); + let aiMessage = ''; + + if (reader) { + try { + while (true) { + const {done, value} = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, {stream: true}); + + // 检查是否包含event: end事件 + let processedChunk = chunk; + if (chunk.includes('event: end')) { + // 只处理event: end之前的内容 + const endIndex = chunk.indexOf('event: end'); + processedChunk = chunk.substring(0, endIndex); + + // 处理截取后的内容 + if (processedChunk.startsWith('data:')) { + // 移除data:前缀 + processedChunk = processedChunk.replace(/^data:/, ''); + // 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的 + processedChunk = processedChunk.replace(/^(\n{2,})/, (match) => { + // 删除所有连续的换行符 + return ''; + }); + } + + if (processedChunk) { + aiMessage += processedChunk; + + // 发送流式数据到前端 + this.provider._postMessage({ + command: 'streamData', + data: processedChunk + }); + } + + // 停止继续处理 + break; + } + + if (chunk.startsWith('data:')) { + // 移除data:前缀 + processedChunk = chunk.replace(/^data:/, ''); + // 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的 + processedChunk = processedChunk.replace(/^(\n{2,})/, (match) => { + // 删除所有连续的换行符 + return ''; + }); + } + + aiMessage += processedChunk; + + // 发送流式数据到前端 + this.provider._postMessage({ + command: 'streamData', + data: processedChunk + }); + } + } catch (readError: any) { + // 检查是否是由于取消请求导致的中断 + if (readError.name === 'AbortError' || readError.message.includes('cancel')) { + console.log('请求已被用户中断'); + // 通知前端请求已中断 + this.provider._postMessage({command: 'requestCancelled'}); + // 重置状态但不抛出错误 + this.provider._postMessage({command: 'hideLoading'}); + return; + } + // 如果是其他错误,重新抛出 + throw readError; + } finally { + // 清理reader引用 + this.currentReader = null; + } + + // 流传输完成后,更新会话历史 + currentSessionHistory = [...currentSessionHistory, {role: 'assistant', content: aiMessage}]; + + // 解析AI响应中的多个代码块并生成独立的codeDiff信息 + const codeBlocks = aiMessage.match(/```[\s\S]*?```/g) || []; + + // 存储所有生成的codeDiff + const allCodeDiffs = []; + + for (const block of codeBlocks) { + // 生成codeDiff + const codeDiff = generateCodeDiff(fileContent, block); + + if (codeDiff) { + // @ts-ignore + allCodeDiffs.push(codeDiff); + } + } + + console.log("allCodeDiffs:",allCodeDiffs) + + // 通知前端流传输完成 + this.provider._postMessage({ + command: 'endStream', + content: aiMessage, + codeDiffs: allCodeDiffs // 传递多个codeDiff信息 + }); + + // 如果有代码差异,保存到工作区变更中 + if (allCodeDiffs.length > 0 && fileContent && message.fileContentPath) { + // 为每个codeDiff创建独立的workspaceChange + for (let i = 0; i < allCodeDiffs.length; i++) { + const codeDiff = allCodeDiffs[i]; + const filePath = message.fileContentPath; + + // 创建唯一的工作区变更键 + const changeKey = `${filePath}`; + + + workspaceChanges[changeKey] = { + original: fileContent, + // @ts-ignore + modified: codeDiff.modifiedCode, + status: 'pending' + }; + } + + // 通知 WebView 更新工作区文件列表 + this.provider._postMessage({ + command: 'updateWorkspaceFiles', + files: workspaceChanges + }); + } + } + } catch (error: any) { + // 检查是否是由于取消请求导致的中断 + if (error.name === 'AbortError' || error.message.includes('cancel') || error.message.includes('中断')) { + console.log('请求已被用户中断'); + this.provider._postMessage({command: 'requestCancelled'}); + } else { + this.provider._postMessage({command: 'addMessage', role: 'ai', content: '调用失败:' + error.message}); + } + } + + this.provider._postMessage({command: 'hideLoading'}); + } + + private async handleSelectContextFile() { + // 执行文件选择逻辑 + 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); + + this.provider._postMessage({ + command: 'setContextFile', + fileName: selectedFileUri.fsPath, + fileContent: decodedContent + }); + } + } + + private async handleGetFileList() { + // 执行获取文件列表逻辑 + 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 // 简化处理,实际可以根据扩展名判断 + }; + }); + + this.provider._postMessage({ + command: 'fileList', + files: files + }); + } + } + + private async handleSelectFileByPath(message: any) { + // 根据路径选择文件 + try { + const fileUri = vscode.Uri.file(message.filePath); + const fileContent = await vscode.workspace.fs.readFile(fileUri); + const decodedContent = new TextDecoder("utf-8").decode(fileContent); + + this.provider._postMessage({ + command: 'setContextFile', + fileName: fileUri.fsPath, + fileContent: decodedContent + }); + } catch (error: any) { + vscode.window.showErrorMessage(`无法读取文件: ${error.message}`); + } + } + + private async handleShowCodeDiffInEditor(message: any) { + // 在编辑器中显示代码差异 + try { + const originalCode = message.originalCode || ''; + const modifiedCode = message.modifiedCode || ''; + + // 创建原始代码的虚拟文档 + const originalUri = vscode.Uri.parse('untitled:original-code.txt'); + const originalDoc = await vscode.workspace.openTextDocument(originalUri); + const originalEditor = await vscode.window.showTextDocument(originalDoc, vscode.ViewColumn.One, true); + await originalEditor.edit(edit => { + edit.insert(new vscode.Position(0, 0), originalCode); + }); + + // 创建修改后代码的虚拟文档 + const modifiedUri = vscode.Uri.parse('untitled:modified-code.txt'); + const modifiedDoc = await vscode.workspace.openTextDocument(modifiedUri); + const modifiedEditor = await vscode.window.showTextDocument(modifiedDoc, vscode.ViewColumn.Two, true); + await modifiedEditor.edit(edit => { + edit.insert(new vscode.Position(0, 0), modifiedCode); + }); + + // 使用 VS Code 的 diff 命令显示差异 + await vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + modifiedUri, + 'Code Diff - Original ↔ Modified' + ); + } catch (error: any) { + vscode.window.showErrorMessage(`显示代码差异失败: ${error.message}`); + } + } + + private async handleAcceptChanges(message: any) { + // 接受代码变更 + try { + const {filePath, modifiedContent} = message; + const fileUri = vscode.Uri.file(filePath); + + // 将修改后的内容写入文件 + const encodedContent = new TextEncoder().encode(modifiedContent); + await vscode.workspace.fs.writeFile(fileUri, encodedContent); + + // 更新工作区变更状态 + if (workspaceChanges[filePath]) { + workspaceChanges[filePath].status = 'accepted'; + } + + // 通知 WebView 更新工作区文件列表 + this.provider._postMessage({ + command: 'updateWorkspaceFiles', + files: workspaceChanges + }); + + vscode.window.showInformationMessage('代码变更已应用'); + } catch (error: any) { + vscode.window.showErrorMessage(`应用代码变更失败: ${error.message}`); + } + } + + private async handleRejectChanges(message: any) { + // 拒绝代码变更 + try { + const {filePath} = message; + + // 更新工作区变更状态 + if (workspaceChanges[filePath]) { + workspaceChanges[filePath].status = 'rejected'; + } + + // 通知 WebView 更新工作区文件列表 + this.provider._postMessage({ + command: 'updateWorkspaceFiles', + files: workspaceChanges + }); + + vscode.window.showInformationMessage('代码变更已拒绝'); + } catch (error: any) { + vscode.window.showErrorMessage(`拒绝代码变更失败: ${error.message}`); + } + } + + private async handleOpenWorkspaceFile(message: any) { + // 打开工作区文件 + try { + const {filePath} = message; + const fileUri = vscode.Uri.file(filePath); + + // 检查文件是否已经打开 + let fileAlreadyOpen = false; + const openedTextDocuments = vscode.workspace.textDocuments; + + for (const doc of openedTextDocuments) { + if (doc.uri.fsPath === fileUri.fsPath) { + fileAlreadyOpen = true; + // 激活已打开的文件 + await vscode.window.showTextDocument(doc, { + viewColumn: vscode.ViewColumn.One // 切换到已打开的文件 + }); + break; + } + } + + // 如果文件未打开,则在新窗口打开 + if (!fileAlreadyOpen) { + await vscode.window.showTextDocument(fileUri, { + viewColumn: vscode.ViewColumn.One // 在第一列打开文件 + }); + } + } catch (error: any) { + vscode.window.showErrorMessage(`打开文件失败: ${error.message}`); + } + } + + private handleGetCodeChanges() { + // 获取代码变更 + this.provider._postMessage({ + command: 'codeChanges', + changes: workspaceChanges + }); + } + + private async handleCreateNewFile(message: any) { + // 创建新文件功能 + try { + const {fileName, content, language} = message; + + // 获取根工作区路径 + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + vscode.window.showErrorMessage('未找到工作区'); + return; + } + + // 根据语言确定文件扩展名 + const fileExtension = getFileExtension(language); + const fullFileName = fileName.endsWith(fileExtension) ? fileName : `${fileName}${fileExtension}`; + + // 创建文件路径(在根目录下) + const filePath = path.join(workspaceFolders[0].uri.fsPath, fullFileName); + const fileUri = vscode.Uri.file(filePath); + + // 将内容写入文件 + const encodedContent = new TextEncoder().encode(content); + await vscode.workspace.fs.writeFile(fileUri, encodedContent); + + // 在编辑器中打开新创建的文件 + await vscode.window.showTextDocument(fileUri, { + viewColumn: vscode.ViewColumn.One + }); + + vscode.window.showInformationMessage(`文件 ${fullFileName} 已创建`); + } catch (error: any) { + vscode.window.showErrorMessage(`生成代码文件失败: ${error.message}`); + } + } + + private handleRestoreState(message: any) { + // 恢复面板状态 + currentSessionHistory = message.history || []; + + // 恢复显示历史消息 + currentSessionHistory.forEach(msg => { + this.provider._postMessage({ + command: 'addMessage', + role: msg.role, + content: msg.content, + codeDiff: null + }); + }); + + // 恢复工作区变更 + if (message.workspaceChanges) { + this.provider._postMessage({ + command: 'updateWorkspaceFiles', + files: message.workspaceChanges + }); + } + } + + // 添加处理取消请求的方法 + private async handleCancelRequest() { + console.log('收到取消请求'); + if (this.currentReader) { + try { + await this.currentReader.cancel(); + this.currentReader = null; + console.log('请求已取消'); + } catch (error) { + console.error('取消请求时出错:', error); + } + } + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index e3d244b..d3726a1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,18 +1,5 @@ import * as vscode from 'vscode'; -import * as path from 'path'; -import {callQwenAPI} from "./utils/modelApi"; -import {generateCodeDiff,getFileExtension} from "./utils/common"; - -// 定义全局变量来存储面板状态 -let currentSessionHistory: { role: string; content: string }[] = []; -// 存储工作区变更的文件,包含状态信息 -let workspaceChanges: { - [key: string]: { - original: string; - modified: string; - status: 'pending' | 'accepted' | 'rejected' - } -} = {}; +import {AISidebarViewProvider} from "./AISidebarViewProvider" export function activate(context: vscode.ExtensionContext) { console.log('Extension activated'); @@ -30,7 +17,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.registerWebviewViewProvider( 'ai-chat-sidebar-view', provider, - { webviewOptions: { retainContextWhenHidden: true } } + {webviewOptions: {retainContextWhenHidden: true}} ) ); @@ -64,448 +51,3 @@ export function activate(context: vscode.ExtensionContext) { // 添加到 subscriptions context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand); } - -class AISidebarViewProvider implements vscode.WebviewViewProvider { - public static readonly viewType = 'ai-chat-sidebar-view'; - - private _view?: vscode.WebviewView; - private _extensionPath: string; - private _context: vscode.ExtensionContext; - - constructor(extensionPath: string, context: vscode.ExtensionContext) { - this._extensionPath = extensionPath; - this._context = context; - } - - 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')) - ] - }; - - webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); - - webviewView.webview.onDidReceiveMessage(async (message) => { - switch (message.command) { - case 'ask': - const question = message.text; - const fileContent = message.fileContent; - const history = currentSessionHistory; - - currentSessionHistory = [...history, { role: 'user', content: question }]; - - this._postMessage({ command: 'addMessage', role: 'user', content: question }); - - try { - const response = await callQwenAPI(question, history, fileContent, this._context); - console.log("response:", response); - const aiMessage = response.choices[0]?.message?.content || '未获取到回复'; - - // 检查是否包含代码修改 - const codeDiff = generateCodeDiff(fileContent, aiMessage); - console.log("codeDiff:",codeDiff) - - // 如果有代码差异,保存到工作区变更中 - if (codeDiff && fileContent && message.fileContentPath) { - workspaceChanges[message.fileContentPath] = { - original: fileContent, - modified: codeDiff.modifiedCode, - status: 'pending' - }; - - // 通知 WebView 更新工作区文件列表 - this._postMessage({ - command: 'updateWorkspaceFiles', - files: workspaceChanges - }); - } - - // 更新当前会话历史 - currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }]; - - this._postMessage({ - command: 'addMessage', - role: 'ai', - content: aiMessage, - codeDiff: codeDiff // 发送代码差异信息 - }); - } catch (error: any) { - this._postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message }); - } - - this._postMessage({ command: 'hideLoading' }); - break; - case 'selectContextFile': - // 执行文件选择逻辑 - 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); - - this._postMessage({ - command: 'setContextFile', - fileName: selectedFileUri.fsPath, - fileContent: decodedContent - }); - } - break; - case 'getFileList': - // 执行获取文件列表逻辑 - 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 // 简化处理,实际可以根据扩展名判断 - }; - }); - - this._postMessage({ - command: 'fileList', - files: files - }); - } - break; - - case 'selectFileByPath': - // 根据路径选择文件 - try { - const fileUri = vscode.Uri.file(message.filePath); - const fileContent = await vscode.workspace.fs.readFile(fileUri); - const decodedContent = new TextDecoder("utf-8").decode(fileContent); - - this._postMessage({ - command: 'setContextFile', - fileName: fileUri.fsPath, - fileContent: decodedContent - }); - } catch (error: any) { - vscode.window.showErrorMessage(`无法读取文件: ${error.message}`); - } - break; - case 'showCodeDiffInEditor': - // 在编辑器中显示代码差异 - try { - const originalCode = message.originalCode || ''; - const modifiedCode = message.modifiedCode || ''; - - // 创建原始代码的虚拟文档 - const originalUri = vscode.Uri.parse('untitled:original-code.txt'); - const originalDoc = await vscode.workspace.openTextDocument(originalUri); - const originalEditor = await vscode.window.showTextDocument(originalDoc, vscode.ViewColumn.One, true); - await originalEditor.edit(edit => { - edit.insert(new vscode.Position(0, 0), originalCode); - }); - - // 创建修改后代码的虚拟文档 - const modifiedUri = vscode.Uri.parse('untitled:modified-code.txt'); - const modifiedDoc = await vscode.workspace.openTextDocument(modifiedUri); - const modifiedEditor = await vscode.window.showTextDocument(modifiedDoc, vscode.ViewColumn.Two, true); - await modifiedEditor.edit(edit => { - edit.insert(new vscode.Position(0, 0), modifiedCode); - }); - - // 使用 VS Code 的 diff 命令显示差异 - await vscode.commands.executeCommand( - 'vscode.diff', - originalUri, - modifiedUri, - 'Code Diff - Original ↔ Modified' - ); - } catch (error: any) { - vscode.window.showErrorMessage(`显示代码差异失败: ${error.message}`); - } - break; - case 'acceptChanges': - // 接受代码变更 - try { - const { filePath, modifiedContent } = message; - const fileUri = vscode.Uri.file(filePath); - - // 将修改后的内容写入文件 - const encodedContent = new TextEncoder().encode(modifiedContent); - await vscode.workspace.fs.writeFile(fileUri, encodedContent); - - // 更新工作区变更状态 - if (workspaceChanges[filePath]) { - workspaceChanges[filePath].status = 'accepted'; - } - - // 通知 WebView 更新工作区文件列表 - this._postMessage({ - command: 'updateWorkspaceFiles', - files: workspaceChanges - }); - - vscode.window.showInformationMessage('代码变更已应用'); - } catch (error: any) { - vscode.window.showErrorMessage(`应用代码变更失败: ${error.message}`); - } - break; - case 'rejectChanges': - // 拒绝代码变更 - try { - const { filePath } = message; - - // 更新工作区变更状态 - if (workspaceChanges[filePath]) { - workspaceChanges[filePath].status = 'rejected'; - } - - // 通知 WebView 更新工作区文件列表 - this._postMessage({ - command: 'updateWorkspaceFiles', - files: workspaceChanges - }); - - vscode.window.showInformationMessage('代码变更已拒绝'); - } catch (error: any) { - vscode.window.showErrorMessage(`拒绝代码变更失败: ${error.message}`); - } - break; - case 'openWorkspaceFile': - // 打开工作区文件 - try { - const { filePath } = message; - const fileUri = vscode.Uri.file(filePath); - - // 检查文件是否已经打开 - let fileAlreadyOpen = false; - const openedTextDocuments = vscode.workspace.textDocuments; - - for (const doc of openedTextDocuments) { - if (doc.uri.fsPath === fileUri.fsPath) { - fileAlreadyOpen = true; - // 激活已打开的文件 - await vscode.window.showTextDocument(doc, { - viewColumn: vscode.ViewColumn.One // 切换到已打开的文件 - }); - break; - } - } - - // 如果文件未打开,则在新窗口打开 - if (!fileAlreadyOpen) { - await vscode.window.showTextDocument(fileUri, { - viewColumn: vscode.ViewColumn.One // 在第一列打开文件 - }); - } - } catch (error: any) { - vscode.window.showErrorMessage(`打开文件失败: ${error.message}`); - } - break; - case 'getCodeChanges': - // 获取代码变更 - this._postMessage({ - command: 'codeChanges', - changes: workspaceChanges - }); - break; - case 'createNewFile': - // 创建新文件功能 - try { - const { fileName, content, language } = message; - - // 获取根工作区路径 - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - vscode.window.showErrorMessage('未找到工作区'); - return; - } - - // 根据语言确定文件扩展名 - const fileExtension = getFileExtension(language); - const fullFileName = fileName.endsWith(fileExtension) ? fileName : `${fileName}${fileExtension}`; - - // 创建文件路径(在根目录下) - const filePath = path.join(workspaceFolders[0].uri.fsPath, fullFileName); - const fileUri = vscode.Uri.file(filePath); - - // 将内容写入文件 - const encodedContent = new TextEncoder().encode(content); - await vscode.workspace.fs.writeFile(fileUri, encodedContent); - - // 在编辑器中打开新创建的文件 - await vscode.window.showTextDocument(fileUri, { - viewColumn: vscode.ViewColumn.One - }); - - vscode.window.showInformationMessage(`文件 ${fullFileName} 已创建`); - } catch (error: any) { - vscode.window.showErrorMessage(`生成代码文件失败: ${error.message}`); - } - break; - case 'restoreState': - // 恢复面板状态 - currentSessionHistory = message.history || []; - - // 恢复显示历史消息 - currentSessionHistory.forEach(msg => { - this._postMessage({ - command: 'addMessage', - role: msg.role, - content: msg.content, - codeDiff: null - }); - }); - - // 恢复工作区变更 - if (message.workspaceChanges) { - this._postMessage({ - command: 'updateWorkspaceFiles', - files: message.workspaceChanges - }); - } - break; - } - }); - } - - public sendSelectedCode(code: string) { - if (this._view) { - this._view.show?.(true); - this._postMessage({ - command: 'addToInput', - role: 'user', - content: code - }); - } - } - - 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); - } - } -} - -function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlightScriptUri:vscode.Uri,highlightStyleUri:vscode.Uri,markedScriptUri:vscode.Uri): string { - return ` - - - - - AI Chat - - - - - -
- -
- - -
- - -
- 添加上下文 -
- - -
- - - - - - - - - - - -
- - -
- -
- - - - - `; -} diff --git a/src/utils/common.ts b/src/utils/common.ts index bf568fa..1256328 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -81,3 +81,24 @@ export function getFileExtension(language: string): string { return languageMap[language?.toLowerCase()] || '.txt'; } +// 简单的语言检测 +export function detectLanguage(code) { + // 可以根据代码特征进行简单判断 + if (code.includes('import React') || code.includes('from react')) { + return 'javascript'; // React代码 + } else if (code.includes('public class') || code.includes('private static')) { + return 'java'; + } else if (code.includes('def ') && code.includes(':')) { + return 'python'; + } else if (code.includes('function ') || code.includes('const ') || code.includes('let ')) { + return 'javascript'; + } else if (code.includes('interface ') && code.includes('export ')) { + return 'typescript'; + } else if (code.includes('('url'); + const userBaseUrl = config.get('baseUrl'); + const userApiKey = config.get('key'); + + // 如果用户配置了完整URL,优先使用 + if (userUrl) { + return { + url: userUrl, + apiKey: userApiKey || 'dev-token' + }; + } + + // 如果用户配置了基础URL,使用基础URL加上默认路径 + if (userBaseUrl) { + return { + url: `${userBaseUrl}/comp/api/v1/chat/completions-stream`, + apiKey: userApiKey || 'dev-token' + }; + } + + // 检测code-server环境 + const codeServerUrl = process.env.CODE_SERVER_URL; + if (codeServerUrl) { + // 在code-server环境中,尝试使用相对路径或者基于当前地址的API地址 + try { + const baseUrl = new URL(codeServerUrl); + return { + url: `${baseUrl.origin}/comp/api/v1/chat/completions-stream`, + apiKey: userApiKey || 'dev-token' + }; + } catch (e) { + // 如果解析失败,使用默认地址 + } + } + + // 检查CODE_SERVER_CONFIG环境变量 + const codeServerConfig = process.env.CODE_SERVER_CONFIG; + if (codeServerConfig) { + try { + // 尝试从配置中解析bindAddr + const configObj = JSON.parse(codeServerConfig); + if (configObj.bindAddr) { + const [host, port] = configObj.bindAddr.split(':'); + if (host && port) { + return { + url: `http://${host}:${port}/comp/api/v1/chat/completions-stream`, + apiKey: userApiKey || 'dev-token' + }; + } + } + } catch (e) { + // 解析失败则继续使用默认配置 + } + } + + // 默认配置 + return { + url: 'https://p13-ai.ngsk.tech:7001/comp/api/v1/chat/completions-stream', + apiKey: userApiKey || 'dev-token' + }; +} export async function callQwenAPI( question: string, history: any[], fileContent: string, - context: vscode.ExtensionContext + context: vscode.ExtensionContext, + filename: string = "" ): Promise { - const apiKey = 'dev-token'; - const url = 'https://aicomp.ngsk.tech:7001/api/v1/chat/completions'; + const { url, apiKey } = getApiConfig(context); const messages = [ { @@ -17,34 +84,41 @@ export async function callQwenAPI( ...history, { role: 'user', - content: `【当前文件内容】: - \`\`\`${fileContent}\`\`\` - - 【用户提问】: - ${question}` + content: question } ]; - console.log("messages:",messages) + console.log("messages:", messages) try { - const response = await fetch(url, { + const params = { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', + // 'Accept': 'text/event-stream' }, body: JSON.stringify({ - model: 'qwen2.5-local', - messages + model: 'Qwen2_5_Coder', + messages, + context_file: { + filename: filename, + section_type: "code", + content: fileContent + } }) - }); + } + console.log("params:", JSON.stringify(params)) + + // @ts-ignore + const response = await fetch(url, params); if (!response.ok) { + console.log("请求失败:", response) throw new Error('API 请求失败'); } - return await response.json(); + return response; } catch (error: any) { console.error(error); throw error; diff --git a/src/utils/webView.ts b/src/utils/webView.ts new file mode 100644 index 0000000..4abc6e1 --- /dev/null +++ b/src/utils/webView.ts @@ -0,0 +1,88 @@ +import vscode from "vscode"; + +export function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlightScriptUri:vscode.Uri,highlightStyleUri:vscode.Uri,markedScriptUri:vscode.Uri): string { + return ` + + + + + AI Chat + + + + + +
+ +
+ + +
+ + +
+ 添加上下文 +
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ + + + + `; +} \ No newline at end of file