584 lines
19 KiB
TypeScript
584 lines
19 KiB
TypeScript
import * as vscode from "vscode";
|
||
import * as path from "path";
|
||
import * as fs from "fs";
|
||
import { VCDFileServer } from "../services/vcdFileServer";
|
||
|
||
/**
|
||
* VCD 波形查看器自定义编辑器提供者
|
||
*/
|
||
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
|
||
public static register(context: vscode.ExtensionContext): vscode.Disposable {
|
||
const provider = new VCDViewerEditorProvider(context);
|
||
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
||
"ic-coder.vcdViewer",
|
||
provider,
|
||
{
|
||
webviewOptions: {
|
||
retainContextWhenHidden: true,
|
||
},
|
||
}
|
||
);
|
||
return providerRegistration;
|
||
}
|
||
|
||
constructor(private readonly context: vscode.ExtensionContext) {}
|
||
|
||
async openCustomDocument(
|
||
uri: vscode.Uri,
|
||
openContext: vscode.CustomDocumentOpenContext,
|
||
token: vscode.CancellationToken
|
||
): Promise<vscode.CustomDocument> {
|
||
return {
|
||
uri,
|
||
dispose: () => {},
|
||
};
|
||
}
|
||
|
||
async resolveCustomEditor(
|
||
document: vscode.CustomDocument,
|
||
webviewPanel: vscode.WebviewPanel,
|
||
token: vscode.CancellationToken
|
||
): Promise<void> {
|
||
webviewPanel.webview.options = {
|
||
enableScripts: true,
|
||
localResourceRoots: [this.context.extensionUri],
|
||
};
|
||
|
||
// 使用公共工厂方法创建 VCD 查看器实例
|
||
VCDViewerPanel.createFromWebviewPanel(
|
||
webviewPanel,
|
||
this.context.extensionUri,
|
||
document.uri.fsPath
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* VCD 波形查看器面板 (使用 Surfer)
|
||
*/
|
||
export class VCDViewerPanel {
|
||
public static currentPanel: VCDViewerPanel | undefined;
|
||
private readonly _panel: vscode.WebviewPanel;
|
||
private readonly _extensionUri: vscode.Uri;
|
||
private _disposables: vscode.Disposable[] = [];
|
||
private _currentVcdPath: string | undefined;
|
||
private _vcdFileServer: VCDFileServer | undefined;
|
||
|
||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
|
||
this._panel = panel;
|
||
this._extensionUri = extensionUri;
|
||
this._vcdFileServer = vcdFileServer;
|
||
|
||
// 设置初始 HTML 内容
|
||
this._panel.webview.html = this._getLoadingHtml();
|
||
|
||
// 监听面板关闭事件
|
||
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||
|
||
// 监听来自 webview 的消息
|
||
this._panel.webview.onDidReceiveMessage(
|
||
(message) => {
|
||
console.log("[VCDViewerPanel] 收到消息:", message);
|
||
switch (message.command) {
|
||
case "loadVCD":
|
||
if (message.filePath) {
|
||
this.loadVCDFile(message.filePath);
|
||
}
|
||
break;
|
||
case "loaded":
|
||
// Surfer iframe 加载完成,发送 VCD 文件
|
||
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
|
||
if (this._currentVcdPath) {
|
||
this.sendVcdToSurfer(this._currentVcdPath);
|
||
}
|
||
break;
|
||
}
|
||
},
|
||
null,
|
||
this._disposables
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 创建或显示 VCD 查看器面板
|
||
*/
|
||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||
const column = vscode.ViewColumn.One;
|
||
|
||
// 如果已经有面板打开,则显示它
|
||
if (VCDViewerPanel.currentPanel) {
|
||
VCDViewerPanel.currentPanel._panel.reveal(column);
|
||
if (vcdFilePath) {
|
||
VCDViewerPanel.currentPanel.loadVCDFile(vcdFilePath);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 创建新面板
|
||
const panel = vscode.window.createWebviewPanel(
|
||
"vcdViewer",
|
||
"VCD 波形查看器",
|
||
column,
|
||
{
|
||
enableScripts: true,
|
||
retainContextWhenHidden: true,
|
||
localResourceRoots: [extensionUri],
|
||
}
|
||
);
|
||
|
||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||
|
||
// 如果提供了 VCD 文件路径,加载它
|
||
if (vcdFilePath) {
|
||
VCDViewerPanel.currentPanel.loadVCDFile(vcdFilePath);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从已有的 webview panel 创建 VCD 查看器(用于自定义编辑器)
|
||
*/
|
||
public static createFromWebviewPanel(
|
||
panel: vscode.WebviewPanel,
|
||
extensionUri: vscode.Uri,
|
||
vcdFilePath: string
|
||
) {
|
||
const viewer = new VCDViewerPanel(panel, extensionUri);
|
||
viewer.loadVCDFile(vcdFilePath);
|
||
return viewer;
|
||
}
|
||
|
||
/**
|
||
* 加载 VCD 文件
|
||
*/
|
||
public loadVCDFile(vcdFilePath: string) {
|
||
try {
|
||
console.log("[VCDViewerPanel] 开始加载 VCD 文件:", vcdFilePath);
|
||
|
||
// 检查文件是否存在
|
||
if (!fs.existsSync(vcdFilePath)) {
|
||
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
|
||
return;
|
||
}
|
||
|
||
// 保存当前 VCD 路径
|
||
this._currentVcdPath = vcdFilePath;
|
||
console.log("[VCDViewerPanel] VCD 路径已保存:", this._currentVcdPath);
|
||
|
||
// 更新面板标题
|
||
const fileName = path.basename(vcdFilePath);
|
||
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
|
||
|
||
// 设置 HTML 内容
|
||
this._panel.webview.html = this._getWebviewContent();
|
||
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
||
} catch (error) {
|
||
vscode.window.showErrorMessage(
|
||
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||
*/
|
||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||
try {
|
||
// 读取 VCD 文件
|
||
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();
|
||
|
||
// 遇到 $enddefinitions 就停止解析
|
||
if (trimmed.startsWith('$enddefinitions')) {
|
||
break;
|
||
}
|
||
|
||
// 查找 $scope 定义
|
||
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||
if (scopeMatch) {
|
||
const scopeType = scopeMatch[1];
|
||
const scopeName = scopeMatch[2];
|
||
|
||
// 记录顶层 module (depth = 0)
|
||
if (scopeDepth === 0 && scopeType === 'module') {
|
||
scopeStack.push(scopeName);
|
||
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
||
}
|
||
// 记录顶层下的直接子模块 (depth = 1)
|
||
else if (scopeDepth === 1 && scopeType === 'module') {
|
||
const fullPath = [...scopeStack, scopeName];
|
||
scopeNames.push(fullPath.join('.'));
|
||
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
|
||
}
|
||
|
||
scopeDepth++;
|
||
}
|
||
|
||
// 遇到 $upscope 减少深度
|
||
if (trimmed.startsWith('$upscope')) {
|
||
scopeDepth--;
|
||
if (scopeDepth === 0) {
|
||
scopeStack.pop();
|
||
}
|
||
}
|
||
}
|
||
|
||
return scopeNames;
|
||
} catch (error) {
|
||
console.error("[VCDViewerPanel] 解析 VCD 文件失败:", error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送 VCD 文件到 Surfer
|
||
*/
|
||
private sendVcdToSurfer(vcdFilePath: string) {
|
||
try {
|
||
console.log("[VCDViewerPanel] 准备发送 VCD 到 Surfer:", vcdFilePath);
|
||
|
||
if (!this._vcdFileServer) {
|
||
throw new Error("VCD 文件服务器未初始化");
|
||
}
|
||
|
||
// 解析 VCD 文件获取根模块名称
|
||
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||
console.log("[VCDViewerPanel] 解析到的作用域名称:", scopeNames);
|
||
|
||
// 注册文件到 HTTP 服务器
|
||
const fileId = this._vcdFileServer.registerFile(vcdFilePath);
|
||
const httpUrl = this._vcdFileServer.getFileUrl(fileId);
|
||
const fileName = path.basename(vcdFilePath);
|
||
|
||
console.log("[VCDViewerPanel] 文件名:", fileName);
|
||
console.log("[VCDViewerPanel] HTTP URL:", httpUrl);
|
||
|
||
// 使用 LoadUrl 命令通过 HTTP 加载文件
|
||
this._panel.webview.postMessage({
|
||
command: "loadVcdUrl",
|
||
url: httpUrl,
|
||
fileName: fileName,
|
||
scopeNames: scopeNames, // 传递解析到的作用域名称
|
||
});
|
||
|
||
console.log("[VCDViewerPanel] 已发送 loadVcdUrl 消息到 webview");
|
||
} catch (error) {
|
||
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
||
vscode.window.showErrorMessage(
|
||
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理资源
|
||
*/
|
||
public dispose() {
|
||
VCDViewerPanel.currentPanel = undefined;
|
||
|
||
this._panel.dispose();
|
||
|
||
while (this._disposables.length) {
|
||
const disposable = this._disposables.pop();
|
||
if (disposable) {
|
||
disposable.dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取加载中的 HTML
|
||
*/
|
||
private _getLoadingHtml(): string {
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>VCD 波形查看器</title>
|
||
<style>
|
||
body {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100vh;
|
||
margin: 0;
|
||
font-family: var(--vscode-font-family);
|
||
color: var(--vscode-foreground);
|
||
background-color: var(--vscode-editor-background);
|
||
}
|
||
.loading {
|
||
text-align: center;
|
||
}
|
||
.spinner {
|
||
border: 4px solid var(--vscode-progressBar-background);
|
||
border-top: 4px solid var(--vscode-progressBar-foreground);
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 20px;
|
||
}
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<p>正在加载 VCD 波形查看器...</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/**
|
||
* 获取 Webview 的 HTML 内容
|
||
*/
|
||
private _getWebviewContent(): string {
|
||
// 获取 surfer 资源 URI
|
||
const surferJsUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
|
||
);
|
||
const surferWasmUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
|
||
);
|
||
const integrationJsUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
|
||
);
|
||
|
||
return `<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
|
||
<title>Surfer 波形查看器</title>
|
||
|
||
<script>
|
||
// 获取 VS Code API(只能调用一次)
|
||
const vscode = acquireVsCodeApi();
|
||
window.vscode = vscode;
|
||
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;
|
||
|
||
// 加载 VCD URL 的函数
|
||
function loadVcdUrl(data) {
|
||
try {
|
||
console.log('[Webview] ========== 开始加载 VCD URL ==========');
|
||
console.log('[Webview] URL:', data.url);
|
||
console.log('[Webview] Scope names from VCD:', data.scopeNames);
|
||
|
||
// 使用 setTimeout 确保 Surfer 完全准备好
|
||
setTimeout(() => {
|
||
console.log('[Webview] 通过 postMessage 发送 LoadUrl 命令');
|
||
|
||
// 使用 integration.js 提供的标准 LoadUrl 命令
|
||
window.postMessage({
|
||
command: 'LoadUrl',
|
||
url: data.url
|
||
}, '*');
|
||
|
||
console.log('[Webview] ✅ 已发送 LoadUrl 命令');
|
||
|
||
// 等待文件加载完成后,自动添加所有信号
|
||
setTimeout(async () => {
|
||
try {
|
||
console.log('[Webview] Attempting to add all signals automatically');
|
||
|
||
// 使用从 VCD 文件解析出来的作用域名称
|
||
let scopeNamesToTry = [];
|
||
|
||
if (data.scopeNames && data.scopeNames.length > 0) {
|
||
// 使用解析出来的实际子模块路径(例如 "tb.dut")
|
||
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||
console.log('[Webview] Using parsed scope names:', scopeNamesToTry);
|
||
} else {
|
||
// 回退到常见的根作用域名称
|
||
scopeNamesToTry = [
|
||
['top'],
|
||
['testbench'],
|
||
['tb'],
|
||
['test'],
|
||
['dut']
|
||
];
|
||
console.log('[Webview] Using fallback scope names');
|
||
}
|
||
|
||
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('[Webview] Sent AddScope for: ' + scopeName.join('.') + ' (recursive)');
|
||
} catch (e) {
|
||
console.log('[Webview] Failed for scope: ' + scopeName.join('.'), e);
|
||
}
|
||
}
|
||
|
||
// 等待信号加载完成后,自动缩放到全部时间范围
|
||
setTimeout(() => {
|
||
try {
|
||
window.inject_message(JSON.stringify("ZoomToFit"));
|
||
console.log('[Webview] Sent ZoomToFit command');
|
||
} catch (e) {
|
||
console.log('[Webview] ZoomToFit failed:', e);
|
||
}
|
||
}, 500);
|
||
|
||
} catch (e) {
|
||
console.error('[Webview] Failed to add signals:', e);
|
||
}
|
||
}, 1500);
|
||
|
||
}, 100);
|
||
|
||
} catch (error) {
|
||
console.error('[Webview] ❌ 加载 VCD 失败:', error);
|
||
on_surfer_error(error.message + '\\n' + error.stack);
|
||
}
|
||
}
|
||
window.loadVcdUrl = loadVcdUrl;
|
||
</script>
|
||
|
||
<script type="module">
|
||
console.log('[Webview] 开始初始化 Surfer...');
|
||
import init from '${surferJsUri}';
|
||
await init({module_or_path: '${surferWasmUri}'});
|
||
console.log('[Webview] Surfer WASM 已加载');
|
||
|
||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '${surferJsUri}';
|
||
window.inject_message = inject_message;
|
||
window.id_of_name = id_of_name;
|
||
window.draw_text_arrow = draw_text_arrow;
|
||
|
||
console.log('[Webview] Surfer 函数已导入,inject_message 类型:', typeof window.inject_message);
|
||
|
||
// 等待一小段时间确保 Surfer 完全初始化
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
||
window.surferReady = true;
|
||
console.log('[Webview] Surfer 已完全初始化并准备就绪');
|
||
|
||
// 关闭 Surfer 的日志面板(如果打开的话)
|
||
try {
|
||
window.inject_message(JSON.stringify("ToggleLogs"));
|
||
console.log('[Webview] 已发送关闭日志面板命令');
|
||
} catch (e) {
|
||
console.log('[Webview] 关闭日志面板失败:', e);
|
||
}
|
||
|
||
// 如果有待处理的 VCD 数据,现在加载它
|
||
if (window.pendingVcdData) {
|
||
console.log('[Webview] 发现待处理的 VCD 数据,立即加载');
|
||
loadVcdUrl(window.pendingVcdData);
|
||
window.pendingVcdData = null;
|
||
} else {
|
||
console.log('[Webview] 没有待处理的 VCD 数据');
|
||
}
|
||
|
||
// 通知 VS Code surfer 已加载完成
|
||
console.log('[Webview] 发送 loaded 消息到 VS Code');
|
||
window.vscode.postMessage({ command: 'loaded' });
|
||
</script>
|
||
|
||
<style>
|
||
html, body {
|
||
overflow: hidden;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
height: 100%;
|
||
width: 100%;
|
||
background: var(--vscode-editor-background);
|
||
}
|
||
|
||
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: var(--vscode-errorForeground);
|
||
background-color: var(--vscode-inputValidation-errorBackground);
|
||
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="${integrationJsUri}"></script>
|
||
<script>
|
||
register_message_listener();
|
||
|
||
console.log('[Webview] 注册 VS Code 消息监听器');
|
||
// 监听来自 VS Code 扩展的消息(使用 vscode API)
|
||
window.addEventListener('message', event => {
|
||
const message = event.data;
|
||
|
||
// 检查是否来自 VS Code
|
||
if (message.command === 'loadVcdUrl') {
|
||
console.log('[Webview] 收到 VS Code 消息,命令:', message.command);
|
||
console.log('[Webview] Surfer 就绪状态:', window.surferReady);
|
||
|
||
if (window.surferReady) {
|
||
// Surfer 已就绪,立即加载
|
||
loadVcdUrl(message);
|
||
} else {
|
||
// Surfer 未就绪,保存数据等待加载
|
||
console.log('[Webview] Surfer 未就绪,保存数据待加载');
|
||
window.pendingVcdData = message;
|
||
}
|
||
}
|
||
}, true); // 使用捕获阶段,优先于 integration.js 的监听器
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
}
|