feat:预览波形展开新开窗口展示完整波形
This commit is contained in:
@ -14,7 +14,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
initUserService(context);
|
initUserService(context);
|
||||||
|
|
||||||
// 初始化 VCD 文件服务器
|
// 初始化 VCD 文件服务器
|
||||||
const vcdFileServer = new VCDFileServer();
|
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||||
vcdFileServer.start().then((port) => {
|
vcdFileServer.start().then((port) => {
|
||||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@ -90,6 +90,39 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 注册命令:在浏览器中打开 VCD 波形查看器
|
||||||
|
const openVCDViewerInBrowserCommand = vscode.commands.registerCommand(
|
||||||
|
"ic-coder.openVCDViewerInBrowser",
|
||||||
|
async (vcdFilePath?: string) => {
|
||||||
|
if (!vcdFilePath) {
|
||||||
|
const fileUri = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFiles: true,
|
||||||
|
canSelectFolders: false,
|
||||||
|
canSelectMany: false,
|
||||||
|
filters: {
|
||||||
|
"VCD 文件": ["vcd"],
|
||||||
|
"所有文件": ["*"],
|
||||||
|
},
|
||||||
|
title: "选择 VCD 文件",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileUri && fileUri[0]) {
|
||||||
|
vcdFilePath = fileUri[0].fsPath;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册文件到服务器
|
||||||
|
const fileId = vcdFileServer.registerFile(vcdFilePath);
|
||||||
|
const viewerUrl = vcdFileServer.getViewerUrl(fileId);
|
||||||
|
|
||||||
|
// 在默认浏览器中打开
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse(viewerUrl));
|
||||||
|
vscode.window.showInformationMessage(`波形查看器已在浏览器中打开`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 注册命令:用户登录
|
// 注册命令:用户登录
|
||||||
const loginCommand = vscode.commands.registerCommand(
|
const loginCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.login",
|
"ic-coder.login",
|
||||||
@ -186,6 +219,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
openPanelCommand,
|
openPanelCommand,
|
||||||
openChatCommand,
|
openChatCommand,
|
||||||
openVCDViewerCommand,
|
openVCDViewerCommand,
|
||||||
|
openVCDViewerInBrowserCommand,
|
||||||
loginCommand,
|
loginCommand,
|
||||||
logoutCommand,
|
logoutCommand,
|
||||||
// TODO: 等待重新实现这些命令
|
// TODO: 等待重新实现这些命令
|
||||||
|
|||||||
@ -247,10 +247,9 @@ export async function showICHelperPanel(
|
|||||||
vscode.window.showInformationMessage(message.text);
|
vscode.window.showInformationMessage(message.text);
|
||||||
break;
|
break;
|
||||||
case "openWaveformViewer":
|
case "openWaveformViewer":
|
||||||
// 打开波形查看器 - 使用 vscode.open 触发自定义编辑器
|
// 在新列中打开波形查看器
|
||||||
if (message.vcdFilePath) {
|
if (message.vcdFilePath) {
|
||||||
const vcdUri = vscode.Uri.file(message.vcdFilePath);
|
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath);
|
||||||
vscode.commands.executeCommand('vscode.open', vcdUri);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "getVCDInfo":
|
case "getVCDInfo":
|
||||||
|
|||||||
@ -107,7 +107,8 @@ export class VCDViewerPanel {
|
|||||||
* 创建或显示 VCD 查看器面板
|
* 创建或显示 VCD 查看器面板
|
||||||
*/
|
*/
|
||||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||||
const column = vscode.ViewColumn.One;
|
// 在当前活动编辑器旁边打开新列
|
||||||
|
const column = vscode.ViewColumn.Beside;
|
||||||
|
|
||||||
// 如果已经有面板打开,则显示它
|
// 如果已经有面板打开,则显示它
|
||||||
if (VCDViewerPanel.currentPanel) {
|
if (VCDViewerPanel.currentPanel) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VCD 文件 HTTP 服务器
|
* VCD 文件 HTTP 服务器
|
||||||
@ -10,6 +11,11 @@ export class VCDFileServer {
|
|||||||
private server: http.Server | null = null;
|
private server: http.Server | null = null;
|
||||||
private port: number = 0;
|
private port: number = 0;
|
||||||
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
||||||
|
private extensionUri: vscode.Uri;
|
||||||
|
|
||||||
|
constructor(extensionUri: vscode.Uri) {
|
||||||
|
this.extensionUri = extensionUri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动服务器
|
* 启动服务器
|
||||||
@ -73,6 +79,13 @@ export class VCDFileServer {
|
|||||||
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取波形查看器 URL
|
||||||
|
*/
|
||||||
|
public getViewerUrl(fileId: string): string {
|
||||||
|
return `http://127.0.0.1:${this.port}/viewer/${fileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成文件 ID
|
* 生成文件 ID
|
||||||
*/
|
*/
|
||||||
@ -101,7 +114,53 @@ export class VCDFileServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 URL,提取文件 ID
|
// 路由处理
|
||||||
|
if (url.startsWith("/viewer/")) {
|
||||||
|
this.handleViewerRequest(url, res);
|
||||||
|
} else if (url.startsWith("/vcd/")) {
|
||||||
|
this.handleVcdFileRequest(url, res);
|
||||||
|
} else if (url.startsWith("/static/")) {
|
||||||
|
this.handleStaticFileRequest(url, res);
|
||||||
|
} else {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not Found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理查看器页面请求
|
||||||
|
*/
|
||||||
|
private handleViewerRequest(url: string, res: http.ServerResponse): void {
|
||||||
|
const match = url.match(/^\/viewer\/(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = match[1];
|
||||||
|
const filePath = this.vcdFiles.get(fileId);
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("File Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 HTML 页面
|
||||||
|
const html = this.generateViewerHtml(fileId, filePath);
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
"Content-Length": Buffer.byteLength(html),
|
||||||
|
});
|
||||||
|
res.end(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 VCD 文件请求
|
||||||
|
*/
|
||||||
|
private handleVcdFileRequest(url: string, res: http.ServerResponse): void {
|
||||||
const match = url.match(/^\/vcd\/(.+)$/);
|
const match = url.match(/^\/vcd\/(.+)$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
@ -142,4 +201,300 @@ export class VCDFileServer {
|
|||||||
res.end("Internal Server Error");
|
res.end("Internal Server Error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理静态文件请求(Surfer 资源)
|
||||||
|
*/
|
||||||
|
private handleStaticFileRequest(url: string, res: http.ServerResponse): void {
|
||||||
|
const match = url.match(/^\/static\/(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = match[1];
|
||||||
|
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("File Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync(filePath);
|
||||||
|
const contentType = this.getContentType(fileName);
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Content-Length": fileContent.length,
|
||||||
|
});
|
||||||
|
res.end(fileContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[VCDFileServer] 读取静态文件失败:`, error);
|
||||||
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Internal Server Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件的 Content-Type
|
||||||
|
*/
|
||||||
|
private getContentType(fileName: string): string {
|
||||||
|
const ext = path.extname(fileName).toLowerCase();
|
||||||
|
const contentTypes: { [key: string]: string } = {
|
||||||
|
".js": "application/javascript",
|
||||||
|
".wasm": "application/wasm",
|
||||||
|
".html": "text/html",
|
||||||
|
".css": "text/css",
|
||||||
|
};
|
||||||
|
return contentTypes[ext] || "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||||||
|
*/
|
||||||
|
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||||
|
try {
|
||||||
|
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
|
||||||
|
const scopeNames: string[] = [];
|
||||||
|
let scopeDepth = 0;
|
||||||
|
const scopeStack: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('$enddefinitions')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||||||
|
if (scopeMatch) {
|
||||||
|
const scopeType = scopeMatch[1];
|
||||||
|
const scopeName = scopeMatch[2];
|
||||||
|
|
||||||
|
if (scopeDepth === 0 && scopeType === 'module') {
|
||||||
|
scopeStack.push(scopeName);
|
||||||
|
} else if (scopeDepth === 1 && scopeType === 'module') {
|
||||||
|
const fullPath = [...scopeStack, scopeName];
|
||||||
|
scopeNames.push(fullPath.join('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeDepth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('$upscope')) {
|
||||||
|
scopeDepth--;
|
||||||
|
if (scopeDepth === 0) {
|
||||||
|
scopeStack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopeNames;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[VCDFileServer] 解析 VCD 文件失败:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成波形查看器 HTML 页面
|
||||||
|
*/
|
||||||
|
private generateViewerHtml(fileId: string, vcdFilePath: string): string {
|
||||||
|
const vcdUrl = this.getFileUrl(fileId);
|
||||||
|
const fileName = path.basename(vcdFilePath);
|
||||||
|
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||||||
|
const scopeNamesJson = JSON.stringify(scopeNames);
|
||||||
|
|
||||||
|
const htmlPart1 = this.getHtmlPart1(fileName);
|
||||||
|
const htmlPart2 = this.getHtmlPart2(vcdUrl, scopeNamesJson);
|
||||||
|
const htmlPart3 = this.getHtmlPart3();
|
||||||
|
|
||||||
|
return htmlPart1 + htmlPart2 + htmlPart3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlPart1(fileName: string): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
<title>Surfer 波形查看器 - ${fileName}</title>
|
||||||
|
<script>
|
||||||
|
window.surferReady = false;
|
||||||
|
window.pendingVcdData = null;
|
||||||
|
|
||||||
|
function on_surfer_error(msg) {
|
||||||
|
console.log("Surfer error:", msg);
|
||||||
|
document.getElementById("error_message").innerHTML = msg;
|
||||||
|
document.getElementById("error_container").style.display = "block";
|
||||||
|
}
|
||||||
|
window.on_surfer_error = on_surfer_error;
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
console.log('[Browser] 开始初始化 Surfer...');
|
||||||
|
import init from '/static/surfer.js';
|
||||||
|
await init({module_or_path: '/static/surfer_bg.wasm'});
|
||||||
|
console.log('[Browser] Surfer WASM 已加载');
|
||||||
|
|
||||||
|
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/static/surfer.js';
|
||||||
|
window.inject_message = inject_message;
|
||||||
|
window.id_of_name = id_of_name;
|
||||||
|
window.draw_text_arrow = draw_text_arrow;
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
window.surferReady = true;
|
||||||
|
console.log('[Browser] Surfer 已完全初始化并准备就绪');
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.inject_message(JSON.stringify("ToggleLogs"));
|
||||||
|
console.log('[Browser] 已发送关闭日志面板命令');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[Browser] 关闭日志面板失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.pendingVcdData) {
|
||||||
|
console.log('[Browser] 发现待处理的 VCD 数据,立即加载');
|
||||||
|
loadVcdUrl(window.pendingVcdData);
|
||||||
|
window.pendingVcdData = null;
|
||||||
|
}
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string {
|
||||||
|
return `
|
||||||
|
<script>
|
||||||
|
function loadVcdUrl(data) {
|
||||||
|
try {
|
||||||
|
console.log('[Browser] ========== 开始加载 VCD URL ==========');
|
||||||
|
console.log('[Browser] URL:', data.url);
|
||||||
|
console.log('[Browser] Scope names from VCD:', data.scopeNames);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[Browser] 通过 postMessage 发送 LoadUrl 命令');
|
||||||
|
window.postMessage({
|
||||||
|
command: 'LoadUrl',
|
||||||
|
url: data.url
|
||||||
|
}, '*');
|
||||||
|
console.log('[Browser] ✅ 已发送 LoadUrl 命令');
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
console.log('[Browser] 尝试自动添加所有信号');
|
||||||
|
let scopeNamesToTry = [];
|
||||||
|
|
||||||
|
if (data.scopeNames && data.scopeNames.length > 0) {
|
||||||
|
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||||||
|
console.log('[Browser] 使用解析的作用域名称:', scopeNamesToTry);
|
||||||
|
} else {
|
||||||
|
scopeNamesToTry = [['top'], ['testbench'], ['tb'], ['test'], ['dut']];
|
||||||
|
console.log('[Browser] 使用回退作用域名称');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||||||
|
const scopeName = scopeNamesToTry[i];
|
||||||
|
try {
|
||||||
|
const addScopeMsg = {
|
||||||
|
"AddScope": [
|
||||||
|
{"strs": scopeName, "id": {"Wellen": i + 1}},
|
||||||
|
true
|
||||||
|
]
|
||||||
|
};
|
||||||
|
window.inject_message(JSON.stringify(addScopeMsg));
|
||||||
|
console.log('[Browser] 已发送 AddScope: ' + scopeName.join('.'));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[Browser] AddScope 失败: ' + scopeName.join('.'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
window.inject_message(JSON.stringify("ZoomToFit"));
|
||||||
|
console.log('[Browser] 已发送 ZoomToFit 命令');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[Browser] ZoomToFit 失败:', e);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Browser] 添加信号失败:', e);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Browser] ❌ 加载 VCD 失败:', error);
|
||||||
|
on_surfer_error(error.message + '\\n' + error.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.loadVcdUrl = loadVcdUrl;
|
||||||
|
|
||||||
|
// 页面加载完成后自动加载 VCD
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const vcdData = {
|
||||||
|
url: '${vcdUrl}',
|
||||||
|
scopeNames: ${scopeNamesJson}
|
||||||
|
};
|
||||||
|
if (window.surferReady) {
|
||||||
|
loadVcdUrl(vcdData);
|
||||||
|
} else {
|
||||||
|
window.pendingVcdData = vcdData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlPart3(): string {
|
||||||
|
return `
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#error_container {
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
margin: 0px auto;
|
||||||
|
max-width: 980px;
|
||||||
|
color: #f48771;
|
||||||
|
background-color: #5a1d1d;
|
||||||
|
position: relative;
|
||||||
|
height: 90%;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
#error_message {
|
||||||
|
overflow: scroll;
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="the_canvas_id"></canvas>
|
||||||
|
<div id="error_container" style="display: none;">
|
||||||
|
<h3>❌ Surfer 加载失败</h3>
|
||||||
|
<code id="error_message"></code>
|
||||||
|
</div>
|
||||||
|
<script src="/static/integration.js"></script>
|
||||||
|
<script>
|
||||||
|
register_message_listener();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -347,7 +347,7 @@ export function getWaveformPreviewScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开完整波形查看器
|
* 打开完整波形查看器(在新列中)
|
||||||
*/
|
*/
|
||||||
function openFullWaveform(vcdFilePath) {
|
function openFullWaveform(vcdFilePath) {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
|
|||||||
Reference in New Issue
Block a user