353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
import * as vscode from "vscode";
|
||
import * as path from "path";
|
||
import * as fs from "fs";
|
||
|
||
/**
|
||
* VCD 波形查看器面板
|
||
*/
|
||
export class VCDViewerPanel {
|
||
public static currentPanel: VCDViewerPanel | undefined;
|
||
private readonly _panel: vscode.WebviewPanel;
|
||
private readonly _extensionUri: vscode.Uri;
|
||
private _disposables: vscode.Disposable[] = [];
|
||
|
||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
|
||
this._panel = panel;
|
||
this._extensionUri = extensionUri;
|
||
|
||
// 设置初始 HTML 内容
|
||
this._panel.webview.html = this._getLoadingHtml();
|
||
|
||
// 监听面板关闭事件
|
||
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||
|
||
// 监听来自 webview 的消息
|
||
this._panel.webview.onDidReceiveMessage(
|
||
(message) => {
|
||
switch (message.command) {
|
||
case "loadVCD":
|
||
if (message.filePath) {
|
||
this.loadVCDFile(message.filePath);
|
||
}
|
||
break;
|
||
}
|
||
},
|
||
null,
|
||
this._disposables
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 创建或显示 VCD 查看器面板
|
||
*/
|
||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string) {
|
||
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);
|
||
|
||
// 如果提供了 VCD 文件路径,加载它
|
||
if (vcdFilePath) {
|
||
VCDViewerPanel.currentPanel.loadVCDFile(vcdFilePath);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载 VCD 文件
|
||
*/
|
||
public loadVCDFile(vcdFilePath: string) {
|
||
try {
|
||
// 检查文件是否存在
|
||
if (!fs.existsSync(vcdFilePath)) {
|
||
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
|
||
return;
|
||
}
|
||
|
||
// 更新面板标题
|
||
const fileName = path.basename(vcdFilePath);
|
||
this._panel.title = `VCD 波形查看器 - ${fileName}`;
|
||
|
||
// 设置 HTML 内容
|
||
this._panel.webview.html = this._getWebviewContent(vcdFilePath);
|
||
} catch (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(vcdFilePath: string): string {
|
||
// 获取资源 URI
|
||
const vcdromJsUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcdrom.js")
|
||
);
|
||
const vcdWasmUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcd.wasm")
|
||
);
|
||
const fontRegularUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Regular.woff2")
|
||
);
|
||
const fontObliqueUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Oblique.woff2")
|
||
);
|
||
const fontItalicUri = this._panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Italic.woff2")
|
||
);
|
||
|
||
// 读取 VCD 文件内容并转换为 base64
|
||
const vcdContent = fs.readFileSync(vcdFilePath, "utf-8");
|
||
const vcdBase64 = Buffer.from(vcdContent).toString("base64");
|
||
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${this._panel.webview.cspSource}; style-src 'unsafe-inline' ${this._panel.webview.cspSource}; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; img-src ${this._panel.webview.cspSource} data:; connect-src ${this._panel.webview.cspSource};">
|
||
<title>VCD 波形查看器</title>
|
||
<style>
|
||
@font-face {
|
||
font-family: 'Iosevka Drom Web';
|
||
font-display: swap;
|
||
font-weight: 400;
|
||
font-stretch: normal;
|
||
font-style: normal;
|
||
src: url('${fontRegularUri}') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Iosevka Drom Web';
|
||
font-display: swap;
|
||
font-weight: 400;
|
||
font-stretch: normal;
|
||
font-style: oblique;
|
||
src: url('${fontObliqueUri}') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Iosevka Drom Web';
|
||
font-display: swap;
|
||
font-weight: 400;
|
||
font-stretch: normal;
|
||
font-style: italic;
|
||
src: url('${fontItalicUri}') format('woff2');
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Iosevka Drom Web', monospace;
|
||
color: var(--vscode-foreground);
|
||
background-color: var(--vscode-editor-background);
|
||
overflow: hidden;
|
||
}
|
||
|
||
#waveform-container {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
overflow: auto;
|
||
}
|
||
|
||
#waveform1 {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.loading {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100vh;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.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-bottom: 20px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.error-message {
|
||
padding: 20px;
|
||
color: var(--vscode-errorForeground);
|
||
background-color: var(--vscode-inputValidation-errorBackground);
|
||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||
border-radius: 4px;
|
||
margin: 20px;
|
||
}
|
||
</style>
|
||
<script src="${vcdromJsUri}"></script>
|
||
</head>
|
||
<body>
|
||
<div id="waveform-container">
|
||
<div class="loading">
|
||
<div class="spinner"></div>
|
||
<p>正在加载 VCD 波形...</p>
|
||
</div>
|
||
<div id="waveform1"></div>
|
||
</div>
|
||
|
||
<script>
|
||
(async function() {
|
||
try {
|
||
// 设置 WASM 文件路径
|
||
window.wasmBinaryFile = '${vcdWasmUri}';
|
||
|
||
// 解码 base64 VCD 内容
|
||
const vcdBase64 = '${vcdBase64}';
|
||
const vcdContent = atob(vcdBase64);
|
||
|
||
// 隐藏加载提示
|
||
document.querySelector('.loading').style.display = 'none';
|
||
|
||
// 创建一个函数来提供 VCD 数据流
|
||
const vcdProvider = async (handler) => {
|
||
// 将 VCD 内容转换为 Uint8Array
|
||
const encoder = new TextEncoder();
|
||
const vcdData = encoder.encode(vcdContent);
|
||
|
||
// 创建一个 ReadableStream reader
|
||
const stream = new ReadableStream({
|
||
start(controller) {
|
||
controller.enqueue(vcdData);
|
||
controller.close();
|
||
}
|
||
});
|
||
|
||
const reader = stream.getReader();
|
||
|
||
// 调用 handler 并传递 reader
|
||
await handler([{
|
||
key: 'local',
|
||
value: 'waveform.vcd',
|
||
format: 'raw',
|
||
baseName: 'waveform.vcd',
|
||
ext: 'vcd',
|
||
reader: reader
|
||
}]);
|
||
};
|
||
|
||
// 初始化 VCDrom,使用函数回调方式
|
||
if (typeof VCDrom === 'function') {
|
||
await VCDrom('waveform1', vcdProvider);
|
||
} else {
|
||
throw new Error('VCDrom 未正确加载');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('加载 VCD 波形失败:', error);
|
||
document.getElementById('waveform-container').innerHTML =
|
||
'<div class="error-message">' +
|
||
'<h3>❌ 加载 VCD 波形失败</h3>' +
|
||
'<p>' + error.message + '</p>' +
|
||
'<p style="margin-top: 10px;">请确保 VCD 文件格式正确。</p>' +
|
||
'<pre style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.1); overflow: auto;">' + error.stack + '</pre>' +
|
||
'</div>';
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
}
|