Merge branch 'feat/plugin-initialization' into feat/back-to-front
This commit is contained in:
@ -8,18 +8,22 @@ import {
|
||||
handleRenameFile,
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog
|
||||
abortCurrentDialog,
|
||||
} from "../utils/messageHandler";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
|
||||
/**
|
||||
* 创建并显示 IC 助手面板
|
||||
*/
|
||||
export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
export function showICHelperPanel(
|
||||
context: vscode.ExtensionContext,
|
||||
viewColumn?: vscode.ViewColumn
|
||||
) {
|
||||
// 创建WebView面板
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"icCoder", // 面板ID
|
||||
"IC Coder", // 面板标题
|
||||
vscode.ViewColumn.Beside, // 显示在旁边
|
||||
viewColumn || vscode.ViewColumn.Beside, // 默认显示在旁边,但可以指定
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
@ -28,7 +32,11 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
);
|
||||
|
||||
// 设置标签页图标
|
||||
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, "media", "图案(方底).png");
|
||||
panel.iconPath = vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"media",
|
||||
"图案(方底).png"
|
||||
);
|
||||
|
||||
// 获取页面内图标URI
|
||||
const iconUri = panel.webview.asWebviewUri(
|
||||
@ -55,7 +63,12 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
handleRenameFile(panel, message.oldPath, message.newPath);
|
||||
break;
|
||||
case "replaceInFile":
|
||||
handleReplaceInFile(panel, message.filePath, message.searchText, message.replaceText);
|
||||
handleReplaceInFile(
|
||||
panel,
|
||||
message.filePath,
|
||||
message.searchText,
|
||||
message.replaceText
|
||||
);
|
||||
break;
|
||||
case "insertCode":
|
||||
insertCodeToEditor(message.code);
|
||||
@ -63,9 +76,42 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "openWaveformViewer":
|
||||
// 打开波形查看器
|
||||
if (message.vcdFilePath) {
|
||||
VCDViewerPanel.createOrShow(
|
||||
context.extensionUri,
|
||||
message.vcdFilePath
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "getVCDInfo":
|
||||
// 获取 VCD 文件信息
|
||||
if (message.vcdFilePath && message.containerId) {
|
||||
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
|
||||
}
|
||||
break;
|
||||
case "createNewConversation":
|
||||
// 创建新会话 - 在当前编辑器组中打开新标签页
|
||||
showICHelperPanel(context, panel.viewColumn);
|
||||
break;
|
||||
case "loadConversationHistory":
|
||||
// 加载会话历史(暂未实现)
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
history: [],
|
||||
});
|
||||
break;
|
||||
case "selectConversation":
|
||||
// 选择会话(暂未实现)
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(message.askId, message.selected, message.customInput);
|
||||
handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
@ -77,3 +123,182 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
context.subscriptions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 VCD 文件信息
|
||||
*/
|
||||
async function getVCDFileInfo(
|
||||
panel: vscode.WebviewPanel,
|
||||
vcdFilePath: string,
|
||||
containerId: string
|
||||
) {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: "文件不存在",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
const stats = fs.statSync(vcdFilePath);
|
||||
const fileSizeKB = stats.size / 1024;
|
||||
const fileSize =
|
||||
fileSizeKB < 1024
|
||||
? `${fileSizeKB.toFixed(2)} KB`
|
||||
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
|
||||
|
||||
// 读取 VCD 文件内容
|
||||
const content = fs.readFileSync(vcdFilePath, "utf-8");
|
||||
|
||||
// 解析信号数量
|
||||
const varMatches = content.match(/\$var/g);
|
||||
const signalCount = varMatches ? varMatches.length : 0;
|
||||
|
||||
// 解析时间范围
|
||||
let timeRange = "N/A";
|
||||
const timeMatch = content.match(/#(\d+)/g);
|
||||
if (timeMatch && timeMatch.length > 0) {
|
||||
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
timeRange = `${minTime} - ${maxTime}`;
|
||||
}
|
||||
|
||||
// 解析前几个信号的真实数据
|
||||
const signals = parseVCDSignals(content, 3); // 只解析前3个信号
|
||||
|
||||
// 发送信息回前端
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: signalCount.toString(),
|
||||
timeRange: timeRange,
|
||||
fileSize: fileSize,
|
||||
signals: signals, // 添加真实信号数据
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取 VCD 文件信息失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件中的信号数据
|
||||
*/
|
||||
function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
const signals: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
values: Array<{ time: number; value: string }>;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
// 1. 解析信号定义部分
|
||||
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
|
||||
let match;
|
||||
const signalDefs: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
}> = [];
|
||||
|
||||
while (
|
||||
(match = varRegex.exec(content)) !== null &&
|
||||
signalDefs.length < maxSignals
|
||||
) {
|
||||
const width = parseInt(match[2]);
|
||||
const identifier = match[3];
|
||||
const name = match[4].trim();
|
||||
|
||||
signalDefs.push({ name, identifier, width });
|
||||
}
|
||||
|
||||
// 2. 找到数据变化部分的起始位置
|
||||
const dumpvarsIndex = content.indexOf("$dumpvars");
|
||||
if (dumpvarsIndex === -1) {
|
||||
return signals;
|
||||
}
|
||||
|
||||
const dataSection = content.substring(dumpvarsIndex);
|
||||
|
||||
// 3. 解析每个信号的值变化
|
||||
for (const signalDef of signalDefs) {
|
||||
const values: Array<{ time: number; value: string }> = [];
|
||||
let currentTime = 0;
|
||||
|
||||
// 分行处理数据
|
||||
const lines = dataSection.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// 解析时间戳
|
||||
if (trimmedLine.startsWith("#")) {
|
||||
currentTime = parseInt(trimmedLine.substring(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析信号值变化
|
||||
// 格式1: 单比特信号 "0!" 或 "1!"
|
||||
// 格式2: 多比特信号 "b1010 !"
|
||||
if (signalDef.width === 1) {
|
||||
// 单比特信号
|
||||
const singleBitMatch = trimmedLine.match(
|
||||
new RegExp(`^([01xz])${signalDef.identifier}$`)
|
||||
);
|
||||
if (singleBitMatch) {
|
||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||
}
|
||||
} else {
|
||||
// 多比特信号
|
||||
const multiBitMatch = trimmedLine.match(
|
||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`)
|
||||
);
|
||||
if (multiBitMatch) {
|
||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||
}
|
||||
}
|
||||
|
||||
// 限制采样点数量,避免数据过多
|
||||
if (values.length >= 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
signals.push({
|
||||
name: signalDef.name,
|
||||
identifier: signalDef.identifier,
|
||||
width: signalDef.width,
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析 VCD 信号数据失败:", error);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import { readFileContent } from "./readFiles";
|
||||
import {
|
||||
createFile,
|
||||
@ -777,17 +778,24 @@ async function handleVCDGeneration(
|
||||
successMsg += `\n\n仿真输出:\n${result.stdout}`;
|
||||
}
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: successMsg,
|
||||
});
|
||||
|
||||
// 自动打开 VCD 波形查看器
|
||||
// 发送带波形预览的消息
|
||||
if (result.vcdFilePath) {
|
||||
vscode.commands.executeCommand("ic-coder.openVCDViewer", result.vcdFilePath);
|
||||
const fileName = path.basename(result.vcdFilePath);
|
||||
panel.webview.postMessage({
|
||||
command: "vcdGenerated",
|
||||
text: successMsg,
|
||||
vcdFilePath: result.vcdFilePath,
|
||||
fileName: fileName,
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`VCD 文件生成成功,已自动打开波形查看器`
|
||||
`VCD 文件生成成功: ${fileName}`
|
||||
);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: successMsg,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let errorMsg = `❌ ${result.message}`;
|
||||
|
||||
308
src/views/conversationHistoryBar.ts
Normal file
308
src/views/conversationHistoryBar.ts
Normal file
@ -0,0 +1,308 @@
|
||||
/**
|
||||
* 获取会话历史栏的 HTML 内容
|
||||
*/
|
||||
export function getConversationHistoryBarContent(): string {
|
||||
return `
|
||||
<div class="conversation-history-bar">
|
||||
<div class="history-dropdown-container">
|
||||
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
|
||||
<span class="dropdown-label">Past Conversations</span>
|
||||
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="history-dropdown-menu" id="historyDropdownMenu">
|
||||
<div class="history-list" id="historyList">
|
||||
<!-- 会话历史列表将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 CSS 样式
|
||||
*/
|
||||
export function getConversationHistoryBarStyles(): string {
|
||||
return `
|
||||
.conversation-history-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-tab-activeBackground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
flex-shrink: 0;
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
.history-dropdown-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-dropdown-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-input-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.history-dropdown-button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.history-dropdown-button.active .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.history-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
min-width: 300px;
|
||||
max-height: 400px;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.history-dropdown-menu.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.history-item-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.history-item-time {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.history-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.new-conversation-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.new-conversation-button:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.new-conversation-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.history-dropdown-menu::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.history-dropdown-menu::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.history-dropdown-menu::-webkit-scrollbar-thumb {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.history-dropdown-menu::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(128, 128, 128, 0.7);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 JavaScript 脚本
|
||||
*/
|
||||
export function getConversationHistoryBarScript(): string {
|
||||
return `
|
||||
// 会话历史相关变量
|
||||
let conversationHistory = [];
|
||||
let currentConversationId = null;
|
||||
|
||||
// 切换历史记录下拉菜单
|
||||
function toggleHistoryDropdown() {
|
||||
const menu = document.getElementById('historyDropdownMenu');
|
||||
const button = document.querySelector('.history-dropdown-button');
|
||||
|
||||
if (menu.classList.contains('active')) {
|
||||
menu.classList.remove('active');
|
||||
button.classList.remove('active');
|
||||
} else {
|
||||
menu.classList.add('active');
|
||||
button.classList.add('active');
|
||||
// 加载会话历史
|
||||
loadConversationHistory();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载会话历史
|
||||
function loadConversationHistory() {
|
||||
vscode.postMessage({ command: 'loadConversationHistory' });
|
||||
}
|
||||
|
||||
// 渲染会话历史列表
|
||||
function renderConversationHistory(history) {
|
||||
conversationHistory = history;
|
||||
const historyList = document.getElementById('historyList');
|
||||
|
||||
if (!history || history.length === 0) {
|
||||
historyList.innerHTML = '<div class="history-empty">暂无会话历史</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
historyList.innerHTML = history.map(item => \`
|
||||
<div class="history-item"
|
||||
onclick="selectConversation('\${item.id}')">
|
||||
<div class="history-item-title">\${item.title || '未命名会话'}</div>
|
||||
<div class="history-item-time">\${formatTime(item.timestamp)}</div>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 选择会话
|
||||
function selectConversation(conversationId) {
|
||||
currentConversationId = conversationId;
|
||||
vscode.postMessage({
|
||||
command: 'selectConversation',
|
||||
conversationId: conversationId
|
||||
});
|
||||
|
||||
// 关闭下拉菜单
|
||||
const menu = document.getElementById('historyDropdownMenu');
|
||||
const button = document.querySelector('.history-dropdown-button');
|
||||
menu.classList.remove('active');
|
||||
button.classList.remove('active');
|
||||
}
|
||||
|
||||
// 创建新会话
|
||||
function createNewConversation() {
|
||||
vscode.postMessage({ command: 'createNewConversation' });
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
// 小于1分钟
|
||||
if (diff < 60000) {
|
||||
return '刚刚';
|
||||
}
|
||||
// 小于1小时
|
||||
if (diff < 3600000) {
|
||||
return Math.floor(diff / 60000) + '分钟前';
|
||||
}
|
||||
// 小于1天
|
||||
if (diff < 86400000) {
|
||||
return Math.floor(diff / 3600000) + '小时前';
|
||||
}
|
||||
// 小于7天
|
||||
if (diff < 604800000) {
|
||||
return Math.floor(diff / 86400000) + '天前';
|
||||
}
|
||||
|
||||
// 超过7天显示具体日期
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
document.addEventListener('click', (event) => {
|
||||
const container = document.querySelector('.history-dropdown-container');
|
||||
const menu = document.getElementById('historyDropdownMenu');
|
||||
const button = document.querySelector('.history-dropdown-button');
|
||||
|
||||
if (menu && menu.classList.contains('active')) {
|
||||
if (!container.contains(event.target)) {
|
||||
menu.classList.remove('active');
|
||||
button.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
350
src/views/waveformPreviewContent.ts
Normal file
350
src/views/waveformPreviewContent.ts
Normal file
@ -0,0 +1,350 @@
|
||||
/**
|
||||
* 获取波形预览组件的样式内容(纯 CSS,不包含 style 标签)
|
||||
*/
|
||||
export function getWaveformPreviewContent(): string {
|
||||
return `
|
||||
/* 波形预览组件样式 */
|
||||
.waveform-preview {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.waveform-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
.waveform-preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.waveform-preview-title svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vscode-button-background);
|
||||
}
|
||||
.waveform-expand-btn {
|
||||
padding: 4px 12px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.waveform-expand-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.waveform-expand-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.waveform-preview-content {
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.waveform-preview-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
.waveform-preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.waveform-preview-placeholder svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.waveform-info {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.waveform-mini-viewer {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: var(--vscode-editor-background);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.waveform-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取波形预览组件的 JavaScript 代码
|
||||
*/
|
||||
export function getWaveformPreviewScript(): string {
|
||||
return `
|
||||
/**
|
||||
* 创建波形预览组件
|
||||
*/
|
||||
function createWaveformPreview(vcdFilePath, fileName) {
|
||||
const previewDiv = document.createElement('div');
|
||||
previewDiv.className = 'waveform-preview';
|
||||
|
||||
// 头部
|
||||
const header = document.createElement('div');
|
||||
header.className = 'waveform-preview-header';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'waveform-preview-title';
|
||||
title.innerHTML = \`
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M128 512h128l64-128 64 128 64-256 64 384 64-128h320"
|
||||
stroke="currentColor"
|
||||
stroke-width="64"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>波形预览 - \${fileName}</span>
|
||||
\`;
|
||||
|
||||
const expandBtn = document.createElement('button');
|
||||
expandBtn.className = 'waveform-expand-btn';
|
||||
expandBtn.innerHTML = \`
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M342 88.3h340c56.6 0 102.6 46 102.6 102.6v340c0 56.6-46 102.6-102.6 102.6H342c-56.6 0-102.6-46-102.6-102.6v-340c0-56.6 46-102.6 102.6-102.6z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="48"/>
|
||||
<path d="M239.4 390.5v340c0 56.6 46 102.6 102.6 102.6h340"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="48"
|
||||
stroke-linecap="round"/>
|
||||
</svg>
|
||||
展开查看
|
||||
\`;
|
||||
expandBtn.onclick = () => openFullWaveform(vcdFilePath);
|
||||
|
||||
header.appendChild(title);
|
||||
header.appendChild(expandBtn);
|
||||
|
||||
// 内容区域 - 创建一个唯一ID的容器用于显示波形
|
||||
const content = document.createElement('div');
|
||||
content.className = 'waveform-preview-content';
|
||||
|
||||
const miniViewerId = 'waveform-mini-' + Date.now();
|
||||
const miniViewer = document.createElement('div');
|
||||
miniViewer.id = miniViewerId;
|
||||
miniViewer.className = 'waveform-mini-viewer';
|
||||
|
||||
// 添加加载提示
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'waveform-loading';
|
||||
loadingDiv.textContent = '正在加载波形预览...';
|
||||
miniViewer.appendChild(loadingDiv);
|
||||
|
||||
content.appendChild(miniViewer);
|
||||
|
||||
previewDiv.appendChild(header);
|
||||
previewDiv.appendChild(content);
|
||||
|
||||
// 异步加载波形数据
|
||||
loadMiniWaveform(miniViewerId, vcdFilePath, loadingDiv);
|
||||
|
||||
return previewDiv;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载迷你波形预览
|
||||
*/
|
||||
async function loadMiniWaveform(containerId, vcdFilePath, loadingDiv) {
|
||||
try {
|
||||
// 请求 VCD 文件信息
|
||||
vscode.postMessage({
|
||||
command: 'getVCDInfo',
|
||||
vcdFilePath: vcdFilePath,
|
||||
containerId: containerId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载波形预览失败:', error);
|
||||
loadingDiv.textContent = '波形预览加载失败';
|
||||
loadingDiv.style.color = 'var(--vscode-errorForeground)';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染波形预览信息
|
||||
*/
|
||||
function renderWaveformInfo(containerId, vcdInfo) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// 清空容器
|
||||
container.innerHTML = '';
|
||||
|
||||
// 绘制真实波形
|
||||
const waveformSvg = document.createElement('div');
|
||||
waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []);
|
||||
|
||||
container.appendChild(waveformSvg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制真实波形
|
||||
*/
|
||||
function drawRealWaveform(signals) {
|
||||
if (!signals || signals.length === 0) {
|
||||
return \`
|
||||
<svg width="100%" height="80" viewBox="0 0 800 80" style="background: var(--vscode-editor-background);">
|
||||
<text x="400" y="40" fill="var(--vscode-descriptionForeground)" font-size="12" text-anchor="middle">
|
||||
无波形数据
|
||||
</text>
|
||||
</svg>
|
||||
\`;
|
||||
}
|
||||
|
||||
const svgWidth = 800;
|
||||
const svgHeight = Math.max(80, signals.length * 30 + 20);
|
||||
const signalHeight = 20;
|
||||
const signalSpacing = 30;
|
||||
const leftMargin = 80;
|
||||
const rightMargin = 20;
|
||||
const waveformWidth = svgWidth - leftMargin - rightMargin;
|
||||
|
||||
const colors = ['var(--vscode-charts-blue)', 'var(--vscode-charts-green)', 'var(--vscode-charts-orange)'];
|
||||
|
||||
let svgContent = \`<svg width="100%" height="\${svgHeight}" viewBox="0 0 \${svgWidth} \${svgHeight}" style="background: var(--vscode-editor-background);">\`;
|
||||
|
||||
// 绘制每个信号
|
||||
signals.forEach((signal, index) => {
|
||||
const y = 10 + index * signalSpacing;
|
||||
const color = colors[index % colors.length];
|
||||
|
||||
// 绘制信号名称
|
||||
svgContent += \`<text x="5" y="\${y + signalHeight / 2 + 4}" fill="var(--vscode-foreground)" font-size="10" opacity="0.8">\${signal.name}</text>\`;
|
||||
|
||||
// 如果没有值变化数据,显示提示
|
||||
if (!signal.values || signal.values.length === 0) {
|
||||
svgContent += \`<text x="\${leftMargin + waveformWidth / 2}" y="\${y + signalHeight / 2 + 4}" fill="var(--vscode-descriptionForeground)" font-size="9" text-anchor="middle" opacity="0.5">无数据</text>\`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算时间范围
|
||||
const times = signal.values.map(v => v.time);
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
const timeRange = maxTime - minTime || 1;
|
||||
|
||||
// 绘制波形
|
||||
let pathData = '';
|
||||
let lastX = leftMargin;
|
||||
let lastValue = signal.values[0].value;
|
||||
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
const value = point.value;
|
||||
|
||||
if (signal.width === 1) {
|
||||
// 单比特信号 - 绘制数字波形
|
||||
const yHigh = y;
|
||||
const yLow = y + signalHeight;
|
||||
const currentY = (value === '1') ? yHigh : yLow;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${currentY}\`;
|
||||
} else {
|
||||
// 绘制垂直跳变
|
||||
const prevY = (lastValue === '1') ? yHigh : yLow;
|
||||
if (prevY !== currentY) {
|
||||
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
|
||||
} else {
|
||||
pathData += \` L \${x} \${currentY}\`;
|
||||
}
|
||||
}
|
||||
|
||||
lastValue = value;
|
||||
lastX = x;
|
||||
} else {
|
||||
// 多比特信号 - 绘制总线波形(梯形)
|
||||
const yTop = y + 5;
|
||||
const yBottom = y + signalHeight - 5;
|
||||
const transitionWidth = 5;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
} else {
|
||||
// 绘制梯形过渡
|
||||
pathData += \` L \${x - transitionWidth} \${yTop} L \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
}
|
||||
});
|
||||
|
||||
// 延伸到右边界
|
||||
if (signal.width === 1) {
|
||||
const lastY = (lastValue === '1') ? y : (y + signalHeight);
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
|
||||
} else {
|
||||
const yMid = y + signalHeight / 2;
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${yMid}\`;
|
||||
}
|
||||
|
||||
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
});
|
||||
|
||||
// 绘制时间轴
|
||||
const timeAxisY = svgHeight - 5;
|
||||
svgContent += \`<line x1="\${leftMargin}" y1="\${timeAxisY}" x2="\${leftMargin + waveformWidth}" y2="\${timeAxisY}" stroke="var(--vscode-foreground)" stroke-width="1" opacity="0.2"/>\`;
|
||||
|
||||
svgContent += \`</svg>\`;
|
||||
|
||||
return svgContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开完整波形查看器
|
||||
*/
|
||||
function openFullWaveform(vcdFilePath) {
|
||||
vscode.postMessage({
|
||||
command: 'openWaveformViewer',
|
||||
vcdFilePath: vcdFilePath
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在消息中添加波形预览
|
||||
*/
|
||||
function addWaveformPreviewToMessage(messageDiv, vcdFilePath, fileName) {
|
||||
const preview = createWaveformPreview(vcdFilePath, fileName);
|
||||
messageDiv.appendChild(preview);
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -1,3 +1,13 @@
|
||||
import {
|
||||
getWaveformPreviewContent,
|
||||
getWaveformPreviewScript,
|
||||
} from "./waveformPreviewContent";
|
||||
import {
|
||||
getConversationHistoryBarContent,
|
||||
getConversationHistoryBarStyles,
|
||||
getConversationHistoryBarScript,
|
||||
} from "./conversationHistoryBar";
|
||||
|
||||
/**
|
||||
* 获取 WebView 面板的 HTML 内容
|
||||
*/
|
||||
@ -14,7 +24,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
background: var(--vscode-editor-background);
|
||||
color: var(--vscode-foreground);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
@ -22,9 +32,16 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
.header.hidden {
|
||||
display: none;
|
||||
}
|
||||
.header h1 {
|
||||
color: var(--vscode-button-background);
|
||||
@ -36,6 +53,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
.messages {
|
||||
flex: 1;
|
||||
@ -177,6 +195,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: -17px;
|
||||
}
|
||||
.mode-selector {
|
||||
display: flex;
|
||||
@ -190,12 +209,13 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
.mode-selector select {
|
||||
padding: 2px 4px;
|
||||
background: transparent;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.mode-selector select:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
@ -381,6 +401,8 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
${getWaveformPreviewContent()}
|
||||
${getConversationHistoryBarStyles()}
|
||||
.file-editor-section {
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
@ -786,6 +808,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${getConversationHistoryBarContent()}
|
||||
<div class="header">
|
||||
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
|
||||
@ -795,35 +818,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
<div class="file-reader-section">
|
||||
<h3>📁 文件读取</h3>
|
||||
<div class="file-input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="filePathInput"
|
||||
placeholder="输入文件路径(相对或绝对路径)..."
|
||||
/>
|
||||
<button onclick="readFile()">读取文件</button>
|
||||
</div>
|
||||
<div id="fileContent" class="file-content empty">
|
||||
文件内容将在这里显示...
|
||||
</div>
|
||||
<div id="errorMessage" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div id="fileEditorSection" class="file-editor-section">
|
||||
<h3>✏️ 编辑文件: <span id="editingFileName"></span></h3>
|
||||
<textarea id="fileEditorTextarea" class="file-editor-textarea"></textarea>
|
||||
<div class="editor-actions">
|
||||
<button onclick="saveFile()">保存修改</button>
|
||||
<button onclick="cancelEdit()" style="background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground);">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="messages" class="messages">
|
||||
<div class="message bot-message">
|
||||
👋 你好!我是 IC Coder 助手,可以帮你生成代码、回答问题。
|
||||
</div>
|
||||
</div>
|
||||
<div id="messages" class="messages"></div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div id="statusBar" class="status-bar" style="display: none;">
|
||||
@ -1074,10 +1069,20 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
div.appendChild(actionsDiv);
|
||||
} else {
|
||||
div.textContent = text;
|
||||
// 当添加用户消息时,隐藏 header
|
||||
hideHeaderIfNeeded();
|
||||
}
|
||||
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
|
||||
// 添加消息后检查 header 显示状态
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
// 检查是否需要隐藏 header
|
||||
function hideHeaderIfNeeded() {
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function copyMessage(text, button) {
|
||||
@ -1252,6 +1257,23 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
case 'showQuestion':
|
||||
showUserQuestion(message.askId, message.question, message.options);
|
||||
break;
|
||||
case 'vcdGenerated':
|
||||
// VCD 文件生成成功,显示带波形预览的消息
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message bot-message';
|
||||
|
||||
const messageContent = document.createElement('div');
|
||||
messageContent.textContent = message.text;
|
||||
messageDiv.appendChild(messageContent);
|
||||
|
||||
// 添加波形预览组件
|
||||
if (message.vcdFilePath && message.fileName) {
|
||||
addWaveformPreviewToMessage(messageDiv, message.vcdFilePath, message.fileName);
|
||||
}
|
||||
|
||||
messagesEl.appendChild(messageDiv);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
break;
|
||||
case 'fileContent':
|
||||
displayFileContent(message.content, message.filePath);
|
||||
break;
|
||||
@ -1267,6 +1289,38 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
case 'fileUpdateError':
|
||||
addMessage(\`❌ \${message.error}\`, 'bot');
|
||||
break;
|
||||
case 'vcdInfo':
|
||||
// 接收到 VCD 文件信息,渲染波形预览
|
||||
if (message.containerId && message.vcdInfo) {
|
||||
renderWaveformInfo(message.containerId, message.vcdInfo);
|
||||
}
|
||||
break;
|
||||
case 'conversationHistory':
|
||||
// 接收到会话历史数据
|
||||
if (message.history) {
|
||||
renderConversationHistory(message.history);
|
||||
}
|
||||
break;
|
||||
case 'conversationLoaded':
|
||||
// 会话加载成功,清空当前消息并显示历史消息
|
||||
messagesEl.innerHTML = '';
|
||||
if (message.messages && message.messages.length > 0) {
|
||||
message.messages.forEach(msg => {
|
||||
addMessage(msg.text, msg.sender);
|
||||
});
|
||||
}
|
||||
currentConversationId = message.conversationId;
|
||||
break;
|
||||
case 'newConversationCreated':
|
||||
// 新会话创建成功,清空消息区域
|
||||
messagesEl.innerHTML = '';
|
||||
currentConversationId = message.conversationId;
|
||||
// 显示 header
|
||||
const header = document.querySelector('.header');
|
||||
if (header) {
|
||||
header.classList.remove('hidden');
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@ -1553,7 +1607,24 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
// 初始化时调整一次高度
|
||||
autoResizeTextarea();
|
||||
|
||||
// 初始化时检查是否需要显示 header
|
||||
checkHeaderVisibility();
|
||||
|
||||
messageInput.focus();
|
||||
|
||||
// 检查 header 显示状态
|
||||
function checkHeaderVisibility() {
|
||||
const allMessages = messagesEl.querySelectorAll('.message');
|
||||
const header = document.querySelector('.header');
|
||||
if (allMessages.length > 0 && header) {
|
||||
header.classList.add('hidden');
|
||||
} else if (header) {
|
||||
header.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
${getConversationHistoryBarScript()}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
Reference in New Issue
Block a user