From 6eb6e877ae49eab6a2baaa994da6e36ae74d6a32 Mon Sep 17 00:00:00 2001 From: Leedehai <18319900+Leedehai@users.noreply.github.com> Date: Wed, 20 Dec 2023 06:03:09 -0500 Subject: [PATCH] Improve test-helper (#2820) --- tools/test-helper/extension.js | 246 +++++++++++++++++++++++++-------- tools/test-helper/package.json | 116 ++++++++++------ 2 files changed, 266 insertions(+), 96 deletions(-) diff --git a/tools/test-helper/extension.js b/tools/test-helper/extension.js index 4682505c7..60bfe9827 100644 --- a/tools/test-helper/extension.js +++ b/tools/test-helper/extension.js @@ -1,87 +1,225 @@ const vscode = require('vscode') const cp = require('child_process') -function activate(context) { - let panel = null +class TestHelper { + constructor() { + /** @type {vscode.Uri?} */ this.sourceUriOfActivePanel = null + /** @type {Map} */ this.panels = new Map() - function refreshPanel(stdout, stderr) { - const uri = vscode.window.activeTextEditor.document.uri - const { pngPath, refPath } = getPaths(uri) + /** @type {vscode.StatusBarItem} */ this.testRunningStatusBarItem = + vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right) + this.testRunningStatusBarItem.text = "$(loading~spin) Running" + this.testRunningStatusBarItem.backgroundColor = + new vscode.ThemeColor('statusBarItem.warningBackground') + } + /** + * The caller should ensure when this function is called, a text editor is active. + * Note the fake "editor" for the extension's WebView panel is not one. + * @returns {vscode.Uri} + */ + static getActiveDocumentUri() { + const editor = vscode.window.activeTextEditor + if (!editor) { + throw new Error('vscode.window.activeTextEditor is undefined.') + } + return editor.document.uri + } + + /** + * The caller should ensure when this function is called, a WebView panel is active. + * @returns {vscode.Uri} + */ + getSourceUriOfActivePanel() { + // If this function is invoked when user clicks the button from within a WebView + // panel, then the active panel is this panel, and sourceUriOfActivePanel is + // guaranteed to have been updated by that panel's onDidChangeViewState listener. + if (!this.sourceUriOfActivePanel) { + throw new Error('sourceUriOfActivePanel is falsy; is there a focused panel?') + } + return this.sourceUriOfActivePanel + } + + /** @param {vscode.Uri} uri */ + static getImageUris(uri) { + const png = vscode.Uri.file(uri.path + .replace("tests/typ", "tests/png") + .replace(".typ", ".png")) + + const ref = vscode.Uri.file(uri.path + .replace("tests/typ", "tests/ref") + .replace(".typ", ".png")) + + return {png, ref} + } + + /** + * @param {vscode.Uri} uri + * @param {string} stdout + * @param {string} stderr + */ + refreshTestPreviewImpl_(uri, stdout, stderr) { + const {png, ref} = TestHelper.getImageUris(uri) + + const panel = this.panels.get(uri) if (panel && panel.visible) { - console.log('Refreshing WebView') - const pngSrc = panel.webview.asWebviewUri(pngPath) - const refSrc = panel.webview.asWebviewUri(refPath) + console.log(`Refreshing WebView for ${uri.fsPath}`) + const webViewSrcs = { + png: panel.webview.asWebviewUri(png), + ref: panel.webview.asWebviewUri(ref), + } panel.webview.html = '' // Make refresh notable. setTimeout(() => { - panel.webview.html = getWebviewContent(pngSrc, refSrc, stdout, stderr) + if (!panel) { + throw new Error('panel to refresh is falsy after waiting') + } + panel.webview.html = getWebviewContent(webViewSrcs, stdout, stderr) }, 50) } } - const openCmd = vscode.commands.registerCommand("ShortcutMenuBar.testOpen", () => { - panel = vscode.window.createWebviewPanel( - 'testOutput', - 'Test output', + /** @param {vscode.Uri} uri */ + openTestPreview(uri) { + if (this.panels.has(uri)) { + this.panels.get(uri)?.reveal() + return + } + + const newPanel = vscode.window.createWebviewPanel( + 'Typst.test-helper.preview', + uri.path.split('/').pop()?.replace('.typ', '.png') ?? 'Test output', vscode.ViewColumn.Beside, - {} ) + newPanel.onDidChangeViewState(() => { + if (newPanel && newPanel.active && newPanel.visible) { + console.log(`Set sourceUriOfActivePanel to ${uri}`) + this.sourceUriOfActivePanel = uri + } else { + console.log(`Set sourceUriOfActivePanel to null`) + this.sourceUriOfActivePanel = null + } + }) + newPanel.onDidDispose(() => { + console.log(`Delete panel ${uri}`) + this.panels.delete(uri) + if (this.sourceUriOfActivePanel === uri) { + this.sourceUriOfActivePanel = null + } + }) + this.panels.set(uri, newPanel) - refreshPanel("", "") - }) + this.refreshTestPreviewImpl_(uri, "", "") + } - const refreshCmd = vscode.commands.registerCommand("ShortcutMenuBar.testRefresh", () => { - refreshPanel("", "") - }) - - const rerunCmd = vscode.commands.registerCommand("ShortcutMenuBar.testRerun", () => { - const uri = vscode.window.activeTextEditor.document.uri + /** @param {vscode.Uri} uri */ + runTest(uri) { const components = uri.fsPath.split(/tests[\/\\]/) const dir = components[0] const subPath = components[1] + this.testRunningStatusBarItem.show() cp.exec( `cargo test --manifest-path ${dir}/Cargo.toml --all --test tests -- ${subPath}`, (err, stdout, stderr) => { - console.log('Ran tests') - refreshPanel(stdout, stderr) + this.testRunningStatusBarItem.hide() + console.log(`Ran tests ${uri.fsPath}`) + this.refreshTestPreviewImpl_(uri, stdout, stderr) } ) - }) + } - const updateCmd = vscode.commands.registerCommand("ShortcutMenuBar.testUpdate", () => { - const uri = vscode.window.activeTextEditor.document.uri - const { pngPath, refPath } = getPaths(uri) + /** @param {vscode.Uri} uri */ + refreshTestPreview(uri) { + const panel = this.panels.get(uri) + if (panel) { + panel.reveal() + this.refreshTestPreviewImpl_(uri, "", "") + } + } - vscode.workspace.fs.copy(pngPath, refPath, { overwrite: true }).then(() => { - console.log('Copied to reference file') - cp.exec(`oxipng -o max -a ${refPath.fsPath}`, (err, stdout, stderr) => { - refreshPanel(stdout, stderr) + /** @param {vscode.Uri} uri */ + updateTestReference(uri) { + const {png, ref} = TestHelper.getImageUris(uri) + + vscode.workspace.fs.copy(png, ref, {overwrite: true}) + .then(() => { + cp.exec(`oxipng -o max -a ${ref.fsPath}`, (err, stdout, stderr) => { + console.log(`Copied to reference file for ${uri.fsPath}`) + this.refreshTestPreviewImpl_(uri, stdout, stderr) + }) }) - }) - }) + } - context.subscriptions.push(openCmd) - context.subscriptions.push(refreshCmd) - context.subscriptions.push(rerunCmd) - context.subscriptions.push(updateCmd) + /** + * @param {vscode.Uri} uri + * @param {string} webviewSection + */ + copyFilePathToClipboard(uri, webviewSection) { + const {png, ref} = TestHelper.getImageUris(uri) + switch (webviewSection) { + case 'png': + vscode.env.clipboard.writeText(png.fsPath) + break + case 'ref': + vscode.env.clipboard.writeText(ref.fsPath) + break + default: + break + } + } } -function getPaths(uri) { - const pngPath = vscode.Uri.file(uri.path - .replace("tests/typ", "tests/png") - .replace(".typ", ".png")) +/** @param {vscode.ExtensionContext} context */ +function activate(context) { + const manager = new TestHelper(); + context.subscriptions.push(manager.testRunningStatusBarItem) - const refPath = vscode.Uri.file(uri.path - .replace("tests/typ", "tests/ref") - .replace(".typ", ".png")) - - return { pngPath, refPath } + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.openFromSource", () => { + manager.openTestPreview(TestHelper.getActiveDocumentUri()) + })) + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.refreshFromSource", () => { + manager.refreshTestPreview(TestHelper.getActiveDocumentUri()) + })) + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.refreshFromPreview", () => { + manager.refreshTestPreview(manager.getSourceUriOfActivePanel()) + })) + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.runFromSource", () => { + manager.runTest(TestHelper.getActiveDocumentUri()) + })) + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.runFromPreview", () => { + manager.runTest(manager.getSourceUriOfActivePanel()) + })) + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.updateFromSource", () => { + manager.updateTestReference(TestHelper.getActiveDocumentUri()) + })) + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.updateFromPreview", () => { + manager.updateTestReference(manager.getSourceUriOfActivePanel()) + })) + // Context menu: the drop-down menu after right-click. + context.subscriptions.push(vscode.commands.registerCommand( + "Typst.test-helper.copyImageFilePathFromPreviewContext", (e) => { + manager.copyFilePathToClipboard( + manager.getSourceUriOfActivePanel(), e.webviewSection) + })) } -function getWebviewContent(pngSrc, refSrc, stdout, stderr) { +/** + * @param {{png: vscode.Uri, ref: vscode.Uri}} webViewSrcs + * @param {string} stdout + * @param {string} stderr + * @returns {string} + */ +function getWebviewContent(webViewSrcs, stdout, stderr) { + const escape = (text) => text.replace(//g, ">") return ` @@ -118,15 +256,15 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) { -
+

Output

- +

Reference

- +
@@ -140,10 +278,6 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) { ` } -function escape(text) { - return text.replace(//g, ">"); -} - function deactivate() {} -module.exports = { activate, deactivate } +module.exports = {activate, deactivate} diff --git a/tools/test-helper/package.json b/tools/test-helper/package.json index d2578ab94..de0c94565 100644 --- a/tools/test-helper/package.json +++ b/tools/test-helper/package.json @@ -1,81 +1,117 @@ { - "name": "typst-test-helper", + "name": "test-helper", + "publisher": "typst", "displayName": "Typst Test Helper", "description": "Helps to run, compare and update Typst tests.", "version": "0.0.1", "engines": { - "vscode": "^1.53.0" + "vscode": "^1.71.0" }, "categories": [ "Other" ], "activationEvents": [ - "onCommand:ShortcutMenuBar.testOpen", - "onCommand:ShortcutMenuBar.testRefresh", - "onCommand:ShortcutMenuBar.testRerun", - "onCommand:ShortcutMenuBar.testUpdate" + "onCommand:Typst.test-helper.openFromSource", + "onCommand:Typst.test-helper.refreshFromSource", + "onCommand:Typst.test-helper.refreshFromPreview", + "onCommand:Typst.test-helper.runFromSource", + "onCommand:Typst.test-helper.runFromPreview", + "onCommand:Typst.test-helper.updateFromSource", + "onCommand:Typst.test-helper.updateFromPreview", + "onCommand:Typst.test-helper.copyImageFilePathFromPreviewContext" ], "main": "./extension.js", "contributes": { "commands": [ { - "command": "ShortcutMenuBar.testOpen", + "command": "Typst.test-helper.openFromSource", "title": "Open test output", - "category": "ShortcutMenuBar", - "icon": { - "light": "images/open-light.svg", - "dark": "images/open-dark.svg" - } + "category": "Typst.test-helper", + "icon": "$(plus)" }, { - "command": "ShortcutMenuBar.testRefresh", + "command": "Typst.test-helper.refreshFromSource", "title": "Refresh preview", - "category": "ShortcutMenuBar", - "icon": { - "light": "images/refresh-light.svg", - "dark": "images/refresh-dark.svg" - } + "category": "Typst.test-helper", + "icon": "$(refresh)" }, { - "command": "ShortcutMenuBar.testRerun", - "title": "Rerun test", - "category": "ShortcutMenuBar", - "icon": { - "light": "images/rerun-light.svg", - "dark": "images/rerun-dark.svg" - } + "command": "Typst.test-helper.refreshFromPreview", + "title": "Refresh preview", + "category": "Typst.test-helper", + "icon": "$(refresh)" }, { - "command": "ShortcutMenuBar.testUpdate", + "command": "Typst.test-helper.runFromSource", + "title": "Run test", + "category": "Typst.test-helper", + "icon": "$(debug-start)" + }, + { + "command": "Typst.test-helper.runFromPreview", + "title": "Run test", + "category": "Typst.test-helper", + "icon": "$(debug-start)" + }, + { + "command": "Typst.test-helper.updateFromSource", "title": "Update reference image", - "category": "ShortcutMenuBar", - "icon": { - "light": "images/update-light.svg", - "dark": "images/update-dark.svg" - } + "category": "Typst.test-helper", + "icon": "$(save)" + }, + { + "command": "Typst.test-helper.updateFromPreview", + "title": "Update reference image", + "category": "Typst.test-helper", + "icon": "$(save)" + }, + { + "command": "Typst.test-helper.copyImageFilePathFromPreviewContext", + "title": "Copy image file path" } ], "menus": { "editor/title": [ { "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", - "command": "ShortcutMenuBar.testOpen", - "group": "navigation@0" - }, - { - "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", - "command": "ShortcutMenuBar.testRefresh", + "command": "Typst.test-helper.openFromSource", "group": "navigation@1" }, { "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", - "command": "ShortcutMenuBar.testRerun", + "command": "Typst.test-helper.refreshFromSource", "group": "navigation@2" }, { "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", - "command": "ShortcutMenuBar.testUpdate", + "command": "Typst.test-helper.runFromSource", "group": "navigation@3" + }, + { + "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", + "command": "Typst.test-helper.updateFromSource", + "group": "navigation@4" + }, + { + "when": "activeWebviewPanelId == Typst.test-helper.preview", + "command": "Typst.test-helper.refreshFromPreview", + "group": "navigation@1" + }, + { + "when": "activeWebviewPanelId == Typst.test-helper.preview", + "command": "Typst.test-helper.runFromPreview", + "group": "navigation@2" + }, + { + "when": "activeWebviewPanelId == Typst.test-helper.preview", + "command": "Typst.test-helper.updateFromPreview", + "group": "navigation@3" + } + ], + "webview/context": [ + { + "command": "Typst.test-helper.copyImageFilePathFromPreviewContext", + "when": "webviewId == Typst.test-helper.preview && (webviewSection == png || webviewSection == ref)" } ] } @@ -84,4 +120,4 @@ "@types/vscode": "^1.53.0", "@types/node": "^12.11.7" } -} +} \ No newline at end of file