From 6abec8c7b7418ca306fb27603daa2a89daa61fff Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Fri, 9 Jan 2026 19:06:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=A2=84=E8=A7=88=E6=B3=A2=E5=BD=A2?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E6=96=B0=E5=BC=80=E7=AA=97=E5=8F=A3=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=AE=8C=E6=95=B4=E6=B3=A2=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 36 ++- src/panels/ICHelperPanel.ts | 5 +- src/panels/VCDViewerPanel.ts | 3 +- src/services/vcdFileServer.ts | 357 +++++++++++++++++++++++++++- src/views/waveformPreviewContent.ts | 2 +- 5 files changed, 396 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index a430c3c..e1c67b4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,7 +14,7 @@ export function activate(context: vscode.ExtensionContext) { initUserService(context); // 初始化 VCD 文件服务器 - const vcdFileServer = new VCDFileServer(); + const vcdFileServer = new VCDFileServer(context.extensionUri); vcdFileServer.start().then((port) => { console.log(`VCD 文件服务器已启动,端口: ${port}`); }).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( "ic-coder.login", @@ -186,6 +219,7 @@ export function activate(context: vscode.ExtensionContext) { openPanelCommand, openChatCommand, openVCDViewerCommand, + openVCDViewerInBrowserCommand, loginCommand, logoutCommand, // TODO: 等待重新实现这些命令 diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index a3b0f01..fae17a5 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -247,10 +247,9 @@ export async function showICHelperPanel( vscode.window.showInformationMessage(message.text); break; case "openWaveformViewer": - // 打开波形查看器 - 使用 vscode.open 触发自定义编辑器 + // 在新列中打开波形查看器 if (message.vcdFilePath) { - const vcdUri = vscode.Uri.file(message.vcdFilePath); - vscode.commands.executeCommand('vscode.open', vcdUri); + vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath); } break; case "getVCDInfo": diff --git a/src/panels/VCDViewerPanel.ts b/src/panels/VCDViewerPanel.ts index f2d7455..477c4a0 100644 --- a/src/panels/VCDViewerPanel.ts +++ b/src/panels/VCDViewerPanel.ts @@ -107,7 +107,8 @@ export class VCDViewerPanel { * 创建或显示 VCD 查看器面板 */ public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) { - const column = vscode.ViewColumn.One; + // 在当前活动编辑器旁边打开新列 + const column = vscode.ViewColumn.Beside; // 如果已经有面板打开,则显示它 if (VCDViewerPanel.currentPanel) { diff --git a/src/services/vcdFileServer.ts b/src/services/vcdFileServer.ts index 1afff27..4bcdfca 100644 --- a/src/services/vcdFileServer.ts +++ b/src/services/vcdFileServer.ts @@ -1,6 +1,7 @@ import * as http from "http"; import * as fs from "fs"; import * as path from "path"; +import * as vscode from "vscode"; /** * VCD 文件 HTTP 服务器 @@ -10,6 +11,11 @@ export class VCDFileServer { private server: http.Server | null = null; private port: number = 0; private vcdFiles: Map = 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}`; } + /** + * 获取波形查看器 URL + */ + public getViewerUrl(fileId: string): string { + return `http://127.0.0.1:${this.port}/viewer/${fileId}`; + } + /** * 生成文件 ID */ @@ -101,7 +114,53 @@ export class VCDFileServer { 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\/(.+)$/); if (!match) { res.writeHead(404, { "Content-Type": "text/plain" }); @@ -142,4 +201,300 @@ export class VCDFileServer { 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 ` + + + + + Surfer 波形查看器 - ${fileName} + + `; + } + + private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string { + return ` + `; + } + + private getHtmlPart3(): string { + return ` + + + + + + + + +`; + } } diff --git a/src/views/waveformPreviewContent.ts b/src/views/waveformPreviewContent.ts index 5bc1586..73135ea 100644 --- a/src/views/waveformPreviewContent.ts +++ b/src/views/waveformPreviewContent.ts @@ -347,7 +347,7 @@ export function getWaveformPreviewScript(): string { } /** - * 打开完整波形查看器 + * 打开完整波形查看器(在新列中) */ function openFullWaveform(vcdFilePath) { vscode.postMessage({