diff --git a/.vscodeignore b/.vscodeignore index 262a135..feb3459 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -16,4 +16,5 @@ resources/**/*.wasm resources/dide-lsp/server tsconfig.json design -lib \ No newline at end of file +lib +*.vcd \ No newline at end of file diff --git a/images/svg/dark/view.svg b/images/svg/dark/view.svg new file mode 100644 index 0000000..7ccda0f --- /dev/null +++ b/images/svg/dark/view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/svg/light/view.svg b/images/svg/light/view.svg new file mode 100644 index 0000000..7ccda0f --- /dev/null +++ b/images/svg/light/view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/l10n/bundle.l10n.en.json b/l10n/bundle.l10n.en.json index 3a21a12..76ee35d 100644 --- a/l10n/bundle.l10n.en.json +++ b/l10n/bundle.l10n.en.json @@ -11,6 +11,14 @@ "progress.choose-best-download-source": "Choose Best Download Source", "progress.extract-digital-lsp": "Extract Digital LSP", "error.download-digital-lsp": "Fail to download digital lsp server, check your network and reload vscode. You can also visit following site and download manually: https://github.com/Digital-EDA/Digital-IDE/releases/tag/", - "fail.save-file": "fail to save file", + "fail.save-file": "保存文件失败", + "save": "保存", + "save-as-view": "另存为视图文件", + "vcd-view-file": "vcd 视图文件", + "all-file": "所有文件", + "load": "加载", + "load-view-file": "加载视图文件", + "bad-view-file": "视图文件已损坏", + "unexist-direct-vcd-file": "视图文件指向的 vcd 文件不存在", "click.join-qq-group": "Click the link to join the QQ group" } \ No newline at end of file diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 8b910d9..38200f3 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -12,5 +12,13 @@ "progress.extract-digital-lsp": "解压 Digital LSP 语言服务器", "error.download-digital-lsp": "无法下载 Digital LSP 语言服务器,检查你的网络后重启 vscode,或者请手动去下方地址下载 https://github.com/Digital-EDA/Digital-IDE/releases/tag/", "fail.save-file": "保存文件失败", + "save": "保存", + "save-as-view": "另存为视图文件", + "vcd-view-file": "vcd 视图文件", + "all-file": "所有文件", + "load": "加载", + "load-view-file": "加载视图文件", + "bad-view-file": "视图文件已损坏", + "unexist-direct-vcd-file": "视图文件指向的 vcd 文件不存在", "click.join-qq-group": "点击链接加入QQ群一起讨论" } \ No newline at end of file diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index 88f1610..0735aa0 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -11,6 +11,14 @@ "progress.choose-best-download-source": "Choose Best Download Source", "progress.extract-digital-lsp": "Extract Digital LSP", "error.download-digital-lsp": "Fail to download digital lsp server, check your network and reload vscode", - "fail.save-file": "保存文件失敗", + "fail.save-file": "保存文件失败", + "save": "保存", + "save-as-view": "另存为视图文件", + "vcd-view-file": "vcd 视图文件", + "all-file": "所有文件", + "load": "加载", + "load-view-file": "加载视图文件", + "bad-view-file": "视图文件已损坏", + "unexist-direct-vcd-file": "视图文件指向的 vcd 文件不存在", "click.join-qq-group": "点击链接加入QQ群一起讨论" } \ No newline at end of file diff --git a/package.json b/package.json index a3ba997..885b1e1 100644 --- a/package.json +++ b/package.json @@ -669,7 +669,7 @@ "group": "navigation@3" }, { - "when": "editorLangId == vcd", + "when": "editorLangId == vcd || editorLangId == view", "command": "digital-ide.waveviewer.show", "group": "navigation@4" }, @@ -716,7 +716,7 @@ "group": "navigation@6" }, { - "when": "resourceLangId == vcd", + "when": "resourceLangId == vcd || resourceLangId == vcd", "command": "digital-ide.waveviewer.show", "group": "navigation@7" }, @@ -763,7 +763,7 @@ "group": "navigation@9" }, { - "when": "resourceLangId == vcd", + "when": "resourceLangId == vcd || resourceLangId == view", "command": "digital-ide.waveviewer.show", "group": "navigation@10" }, @@ -791,6 +791,9 @@ "selector": [ { "filenamePattern": "*.vcd" + }, + { + "filenamePattern": "*.view" } ], "priority": "default" @@ -971,6 +974,16 @@ "light": "./images/svg/light/vcd.svg" } }, + { + "id": "view", + "extensions": [ + ".view" + ], + "icon": { + "dark": "./images/svg/dark/view.svg", + "light": "./images/svg/light/view.svg" + } + }, { "id": "digital-ide-output", "mimetypes": [ diff --git a/src/function/dide-viewer/api.ts b/src/function/dide-viewer/api.ts index 781fd4d..e4fb3af 100644 --- a/src/function/dide-viewer/api.ts +++ b/src/function/dide-viewer/api.ts @@ -1,7 +1,172 @@ import * as fs from 'fs'; +import * as vscode from 'vscode'; +import * as url from 'url'; import { BSON } from 'bson'; -import { hdlPath } from '../../hdlFs'; +import * as path from 'path'; -export async function saveView(file: string, payload: any) { +export interface SaveViewData { + originVcdFile: string, + originVcdViewFile: string, + payload: any +} + +export interface LaunchFiles { + vcd: string, + view: string, + worker: string, + root: string +} + +const payloadCache = new Map(); + +function mergePayloadCache(file: string, payload: any) { + if (!payloadCache.has(file)) { + payloadCache.set(file, payload); + } + const originPayload = payloadCache.get(file); + Object.assign(originPayload, payload); + return originPayload; +} + +function extractFilepath(webviewUri: string) { + if (webviewUri === undefined || webviewUri.length === 0) { + return ''; + } + const parsedUrl = new url.URL(webviewUri); + const pathname = decodeURIComponent(parsedUrl.pathname); + if (pathname.startsWith('/')) { + return pathname.slice(1); + } + return pathname; +} + +// api 与 https://github.com/Digital-EDA/digital-vcd-backend 同构 +export async function saveView(data: any, uri: vscode.Uri, panel: vscode.WebviewPanel) { + try { + let { originVcdFile, originVcdViewFile, payload } = data as SaveViewData; + + // webview uri 转换为绝对路径 + originVcdFile = extractFilepath(originVcdFile); + originVcdViewFile = extractFilepath(originVcdViewFile); + + const rootPath = path.dirname(uri.fsPath); + payload.originVcdFile = path.isAbsolute(originVcdFile) ? originVcdFile : path.join(rootPath, originVcdFile).replace(/\\/g, '/'); + const originPayload = mergePayloadCache(originVcdViewFile, payload); + + const savePath = path.isAbsolute(originVcdViewFile) ? originVcdViewFile : path.join(rootPath, originVcdViewFile); + const buffer = BSON.serialize(originPayload); + fs.writeFileSync(savePath, buffer); + } catch (error) { + console.error('error happen in saveView ' + error); + } +} + +function getFilename(file: string) { + const base = path.basename(file); + const spls = base.split('.'); + if (spls.length === 1) { + return base; + } + return spls[0]; +} + + +export async function saveViewAs(data: any, uri: vscode.Uri, panel: vscode.WebviewPanel) { + const { t } = vscode.l10n; + + try { + // 先保存原来的文件 payload 一定是 all + let { originVcdFile, originVcdViewFile, payload } = data; + + // webview uri 转换为绝对路径 + originVcdFile = extractFilepath(originVcdFile); + originVcdViewFile = extractFilepath(originVcdViewFile); + + const rootPath = path.dirname(uri.fsPath); + payload.originVcdFile = path.isAbsolute(originVcdFile) ? originVcdFile : path.join(rootPath, originVcdFile).replace(/\\/g, '/'); + + const originPayload = mergePayloadCache(originVcdViewFile, payload); + + // 询问新的路径 + const defaultFilename = getFilename(payload.originVcdFile); + const vcdFilters: Record = {}; + vcdFilters[t('vcd-view-file')] = ['view']; + vcdFilters[t('all-file')] = ['*']; + + const saveUri = await vscode.window.showSaveDialog({ + title: t('save-as-view'), + defaultUri: vscode.Uri.file(path.join(rootPath, defaultFilename)), + saveLabel: t('save'), + filters: vcdFilters + }); + + if (saveUri) { + const savePath = saveUri.fsPath; + const buffer = BSON.serialize(originPayload); + fs.writeFileSync(savePath, buffer); + + // 创建新的缓存 savePath 会成为新的 originVcdViewFile + mergePayloadCache(savePath, payload); + + panel.webview.postMessage({ + command: 'save-view-as', + viewPath: savePath.replace(/\\/g, '/') + }); + } else { + panel.webview.postMessage({ + command: 'save-view-as', + viewPath: undefined + }); + } + } catch (error) { + console.error('error happen in saveViewAs ' + error); + } +} + + +export async function loadView(data: any, uri: vscode.Uri, panel: vscode.WebviewPanel) { + const { t } = vscode.l10n; + try { + let { originVcdFile } = data; + + originVcdFile = extractFilepath(originVcdFile); + + const rootPath = path.dirname(uri.fsPath); + const vcdPath = path.isAbsolute(originVcdFile) ? originVcdFile : path.join(rootPath, originVcdFile).replace(/\\/g, '/'); + + const defaultFolder = path.dirname(vcdPath); + const vcdFilters: Record = {}; + vcdFilters[t('vcd-view-file')] = ['view']; + vcdFilters[t('all-file')] = ['*']; + + const viewUri = await vscode.window.showOpenDialog({ + title: t('load-view-file'), + defaultUri: vscode.Uri.file(defaultFolder), + openLabel: t('load'), + canSelectFiles: true, + canSelectMany: false, + canSelectFolders: false, + filters: vcdFilters + }); + + if (viewUri) { + const viewPath = viewUri[0].fsPath; + const buffer = fs.readFileSync(viewPath); + const recoverJson = BSON.deserialize(buffer); + panel.webview.postMessage({ + command: 'load-view', + recoverJson, + viewPath + }); + } else { + panel.webview.postMessage({ + command: 'load-view', + recoverJson: undefined, + viewPath: undefined + }); + } + } catch (error) { + console.error('error happen in loadView ' + error); + } } \ No newline at end of file diff --git a/src/function/dide-viewer/index.ts b/src/function/dide-viewer/index.ts index b1a999c..71d9190 100644 --- a/src/function/dide-viewer/index.ts +++ b/src/function/dide-viewer/index.ts @@ -1,11 +1,14 @@ import * as vscode from 'vscode'; import * as fspath from 'path'; +import * as fs from 'fs'; import { hdlFile, hdlPath } from '../../hdlFs'; import { opeParam, ReportType, WaveViewOutput } from '../../global'; +import { LaunchFiles, loadView, saveView, saveViewAs } from './api'; +import { BSON } from 'bson'; -function getWebviewContent(panel?: vscode.WebviewPanel): string | undefined { - const dideviewerPath = hdlPath.join(opeParam.extensionPath, 'resources', 'dide-viewer', 'view'); +function getWebviewContent(context: vscode.ExtensionContext, panel?: vscode.WebviewPanel): string | undefined { + const dideviewerPath = hdlPath.join(context.extensionPath, 'resources', 'dide-viewer', 'view'); const htmlIndexPath = hdlPath.join(dideviewerPath, 'index.html'); const html = hdlFile.readFile(htmlIndexPath)?.replace(/( { const absLocalPath = fspath.resolve(dideviewerPath, $2); @@ -50,23 +53,26 @@ class WaveViewer { console.log(message); }, null, this.context.subscriptions); - const previewHtml = getWebviewContent(this.panel); + const context = this.context; + const previewHtml = getWebviewContent(context, this.panel); if (this.panel && previewHtml) { - const dideviewerPath = hdlPath.join(opeParam.extensionPath, 'resources', 'dide-viewer', 'view'); - const workerAbsPath = hdlPath.join(dideviewerPath, 'worker.js'); - const webviewUri = this.panel.webview.asWebviewUri(uri); - const workerUri = this.panel.webview.asWebviewUri(vscode.Uri.file(workerAbsPath)); - const workerRootUri = this.panel.webview.asWebviewUri(vscode.Uri.file(dideviewerPath)); + const launchFiles = getViewLaunchFiles(context, uri, this.panel); + if (launchFiles instanceof Error) { + vscode.window.showErrorMessage(launchFiles.message); + return; + } + const { vcd, view, worker, root } = launchFiles; let preprocessHtml = previewHtml - .replace('test.vcd', webviewUri.toString()) - .replace('worker.js', workerUri.toString()) - .replace('', workerRootUri.toString()); + .replace('test.vcd', vcd) + .replace('test.view', view) + .replace('worker.js', worker) + .replace('', root); this.panel.webview.html = preprocessHtml; - const iconPath = hdlPath.join(opeParam.extensionPath, 'images', 'icon.svg'); + const iconPath = hdlPath.join(context.extensionPath, 'images', 'icon.svg'); this.panel.iconPath = vscode.Uri.file(iconPath); - this.registerMessageEvent(); + registerMessageEvent(this.panel, uri); } else { WaveViewOutput.report('preview html in is empty', ReportType.Warn); } @@ -78,21 +84,6 @@ class WaveViewer { }); } - - // vscode 前端接受 webview 的消息 - private registerMessageEvent() { - this.panel?.webview.onDidReceiveMessage(message => { - const { command, data } = message; - - switch (command) { - case 'save-view': - break; - - default: - break; - } - }); - } } async function openWaveViewer(context: vscode.ExtensionContext, uri: vscode.Uri) { @@ -113,8 +104,12 @@ class VcdViewerDocument implements vscode.CustomDocument { class VcdViewerProvider implements vscode.CustomEditorProvider { private readonly _onDidChangeCustomDocument = new vscode.EventEmitter>(); public readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event; + context: vscode.ExtensionContext; + constructor(context: vscode.ExtensionContext) { + this.context = context; + } - async resolveCustomEditor(document: VcdViewerDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken) { + async resolveCustomEditor(document: VcdViewerDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken) { webviewPanel.webview.options = { enableScripts: true, enableForms: true, @@ -124,26 +119,26 @@ class VcdViewerProvider implements vscode.CustomEditorProvider { webviewPanel.dispose(); }, null); - webviewPanel.webview.onDidReceiveMessage(message => { - console.log(message); - }, null); - - const previewHtml = getWebviewContent(webviewPanel); - - if (webviewPanel && previewHtml) { - const dideviewerPath = hdlPath.join(opeParam.extensionPath, 'resources', 'dide-viewer', 'view'); - const workerAbsPath = hdlPath.join(dideviewerPath, 'worker.js'); - const webviewUri = webviewPanel.webview.asWebviewUri(document.uri); - const workerUri = webviewPanel.webview.asWebviewUri(vscode.Uri.file(workerAbsPath)); - const workerRootUri = webviewPanel.webview.asWebviewUri(vscode.Uri.file(dideviewerPath)); + const context = this.context; + const previewHtml = getWebviewContent(context, webviewPanel); + registerMessageEvent(webviewPanel, document.uri); + if (webviewPanel && previewHtml) { + const launchFiles = getViewLaunchFiles(context, document.uri, webviewPanel); + if (launchFiles instanceof Error) { + vscode.window.showErrorMessage(launchFiles.message); + return; + } + + const { vcd, view, worker, root } = launchFiles; let preprocessHtml = previewHtml - .replace('test.vcd', webviewUri.toString()) - .replace('worker.js', workerUri.toString()) - .replace('', workerRootUri.toString()); + .replace('test.vcd', vcd) + .replace('test.view', view) + .replace('worker.js', worker) + .replace('', root); webviewPanel.webview.html = preprocessHtml; - const iconPath = hdlPath.join(opeParam.extensionPath, 'images', 'icon.svg'); + const iconPath = hdlPath.join(context.extensionPath, 'images', 'icon.svg'); webviewPanel.iconPath = vscode.Uri.file(iconPath); } else { WaveViewOutput.report('preview html in is empty', ReportType.Warn); @@ -182,9 +177,71 @@ class VcdViewerProvider implements vscode.CustomEditorProvider { } } -const vcdViewerProvider = new VcdViewerProvider(); +// vscode 前端接受 webview 的消息 +function registerMessageEvent(panel: vscode.WebviewPanel, uri: vscode.Uri) { + panel.webview.onDidReceiveMessage(message => { + const { command, data } = message; + + switch (command) { + case 'save-view': + saveView(data, uri, panel); + break; + case 'save-view-as': + saveViewAs(data, uri, panel); + break; + case 'load-view': + loadView(data, uri, panel); + default: + break; + } + }); +} + + +/** + * @description 准备启动 webview 的基础资源 + * @param context + * @param uri + * @param panel + * @returns + */ +function getViewLaunchFiles(context: vscode.ExtensionContext, uri: vscode.Uri, panel: vscode.WebviewPanel): LaunchFiles | Error { + const { t } = vscode.l10n; + console.log(uri.fsPath); + + const entryPath = uri.fsPath; + const dideviewerPath = hdlPath.join(context.extensionPath, 'resources', 'dide-viewer', 'view'); + const workerAbsPath = hdlPath.join(dideviewerPath, 'worker.js'); + const worker = panel.webview.asWebviewUri(vscode.Uri.file(workerAbsPath)).toString(); + const root = panel.webview.asWebviewUri(vscode.Uri.file(dideviewerPath)).toString(); + + // 根据打开文件的类型来判断资源加载方案 + if (entryPath.endsWith('.vcd')) { + const defaultViewPath = entryPath.slice(0, -4) + '.view'; + const vcd = panel.webview.asWebviewUri(uri).toString(); + const view = panel.webview.asWebviewUri(vscode.Uri.file(defaultViewPath)).toString(); + + return { vcd, view, worker, root }; + } else if (entryPath.endsWith('.view')) { + const buffer = fs.readFileSync(entryPath); + const recoverJson = BSON.deserialize(buffer); + if (recoverJson.originVcdFile) { + const vcdPath = recoverJson.originVcdFile; + if (!fs.existsSync(vcdPath)) { + return new Error(t('unexist-direct-vcd-file') + ':' + vcdPath); + } + const vcd = panel.webview.asWebviewUri(vscode.Uri.file(recoverJson.originVcdFile)).toString(); + const view = panel.webview.asWebviewUri(uri).toString(); + + return { vcd, view, worker, root }; + } else { + return new Error(t('bad-view-file') + ':' + entryPath); + } + } + return new Error('unsupported languages'); +} export { openWaveViewer, - vcdViewerProvider + VcdViewerProvider }; \ No newline at end of file diff --git a/src/function/index.ts b/src/function/index.ts index 3b987d8..305398b 100644 --- a/src/function/index.ts +++ b/src/function/index.ts @@ -130,7 +130,10 @@ function registerNetlist(context: vscode.ExtensionContext) { function registerWaveViewer(context: vscode.ExtensionContext) { vscode.commands.registerCommand('digital-ide.waveviewer.show', uri => WaveView.openWaveViewer(context, uri)); - vscode.window.registerCustomEditorProvider('digital-ide.vcd.viewer', WaveView.vcdViewerProvider, + + // 通过 customEditors 来配置 + const vcdViewerProvider = new WaveView.VcdViewerProvider(context); + vscode.window.registerCustomEditorProvider('digital-ide.vcd.viewer', vcdViewerProvider, { webviewOptions: { retainContextWhenHidden: true,