From 52e3fb523f1ec1a2e039804b82ae3a5f449905a2 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 19 Aug 2023 22:38:51 +0200 Subject: [PATCH 01/31] Add initial stat viewer and message broadcasting --- .prettierignore | 3 - src/extension/editors/base.ts | 15 +- src/extension/editors/index.ts | 2 +- src/extension/editors/messages.ts | 38 ++++ src/extension/editors/nextpnr.ts | 6 +- src/extension/editors/project.ts | 6 +- .../editors/{digitaljs.ts => yosys.ts} | 22 +- src/extension/tasks/rtl.ts | 40 +++- src/extension/types.ts | 19 -- views/digitaljs/src/main.ts | 191 ++++++------------ views/digitaljs/src/messages.ts | 38 ++++ views/digitaljs/src/viewers/base.ts | 39 ++++ views/digitaljs/src/viewers/diagram.ts | 119 +++++++++++ views/digitaljs/src/viewers/index.ts | 2 + views/digitaljs/src/viewers/stats.ts | 33 +++ 15 files changed, 419 insertions(+), 154 deletions(-) create mode 100644 src/extension/editors/messages.ts rename src/extension/editors/{digitaljs.ts => yosys.ts} (78%) delete mode 100644 src/extension/types.ts create mode 100644 views/digitaljs/src/messages.ts create mode 100644 views/digitaljs/src/viewers/base.ts create mode 100644 views/digitaljs/src/viewers/diagram.ts create mode 100644 views/digitaljs/src/viewers/index.ts create mode 100644 views/digitaljs/src/viewers/stats.ts diff --git a/.prettierignore b/.prettierignore index ca87251..849ddff 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1 @@ dist/ - -# TODO: un-ignore this file once we find a solution to the jquery import reordering situation -views/digitaljs/src/main.ts diff --git a/src/extension/editors/base.ts b/src/extension/editors/base.ts index 55c96cf..d7c38c8 100644 --- a/src/extension/editors/base.ts +++ b/src/extension/editors/base.ts @@ -1,9 +1,10 @@ import * as vscode from 'vscode'; import type {Projects} from '../projects/index.js'; -import {type ViewMessage} from '../types.js'; import {getWebviewUri} from '../util.js'; +import {type ViewMessage} from './messages.js'; + export abstract class BaseEditor implements vscode.CustomTextEditorProvider { protected readonly context: vscode.ExtensionContext; protected readonly projects: Projects; @@ -48,6 +49,16 @@ export abstract class BaseEditor implements vscode.CustomTextEditorProvider { } }) ); + // TODO: does not work reliably! + disposables.push( + vscode.window.onDidChangeVisibleTextEditors((event) => { + console.log('New windows:'); + console.log(event); + if (event.map((editor) => editor.document).indexOf(document) !== -1) { + this.onClose(document, webviewPanel.webview); + } + }) + ); // Create file system watcher const watcher = vscode.workspace.createFileSystemWatcher(document.uri.fsPath); @@ -129,5 +140,7 @@ export abstract class BaseEditor implements vscode.CustomTextEditorProvider { protected abstract onSave(document: vscode.TextDocument, webview: vscode.Webview): void; + protected abstract onClose(document: vscode.TextDocument, webview: vscode.Webview): void; + protected abstract update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean): void; } diff --git a/src/extension/editors/index.ts b/src/extension/editors/index.ts index bc86727..c9ebd3e 100644 --- a/src/extension/editors/index.ts +++ b/src/extension/editors/index.ts @@ -1,3 +1,3 @@ -export * from './digitaljs.js'; +export * from './yosys.js'; export * from './nextpnr.js'; export * from './project.js'; diff --git a/src/extension/editors/messages.ts b/src/extension/editors/messages.ts new file mode 100644 index 0000000..3446f9b --- /dev/null +++ b/src/extension/editors/messages.ts @@ -0,0 +1,38 @@ +interface ForeignViewMessageModuleFocus { + type: 'moduleFocus'; + module: string; +} + +export type ForeignViewMessage = ForeignViewMessageModuleFocus; + +interface MessageBroadcast { + type: 'broadcast'; + message: ForeignViewMessage; +} + +interface EditorMessageDocument { + type: 'document'; + document: string; +} + +export type EditorMessage = EditorMessageDocument | MessageBroadcast; + +interface ViewMessageReady { + type: 'ready'; +} + +interface ViewMessageChange { + type: 'change'; + document: string; +} + +interface ViewMessageRequestSave { + type: 'requestSave'; + data: { + fileContents: string; + defaultPath?: string; + filters?: Record; + }; +} + +export type ViewMessage = ViewMessageReady | ViewMessageChange | MessageBroadcast | ViewMessageRequestSave; diff --git a/src/extension/editors/nextpnr.ts b/src/extension/editors/nextpnr.ts index 205d7ce..2136a19 100644 --- a/src/extension/editors/nextpnr.ts +++ b/src/extension/editors/nextpnr.ts @@ -1,9 +1,9 @@ import * as vscode from 'vscode'; -import {type ViewMessage} from '../types.js'; import {getWebviewUri} from '../util.js'; import {BaseEditor} from './base.js'; +import {type ViewMessage} from './messages.js'; export class NextpnrEditor extends BaseEditor { public static getViewType() { @@ -48,6 +48,10 @@ export class NextpnrEditor extends BaseEditor { // Do nothing } + protected onClose(_document: vscode.TextDocument, _webview: vscode.Webview): void { + // Do nothing + } + protected update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { if (!isDocumentChange) { vscode.commands.executeCommand('edacation-projects.focus'); diff --git a/src/extension/editors/project.ts b/src/extension/editors/project.ts index 28d9a8d..846efd8 100644 --- a/src/extension/editors/project.ts +++ b/src/extension/editors/project.ts @@ -1,9 +1,9 @@ import * as vscode from 'vscode'; import {Project} from '../projects/index.js'; -import {type ViewMessage} from '../types.js'; import {BaseEditor} from './base.js'; +import {type ViewMessage} from './messages.js'; export class ProjectEditor extends BaseEditor { public static getViewType() { @@ -66,6 +66,10 @@ export class ProjectEditor extends BaseEditor { this.update(document, webview, false); } + protected onClose(_document: vscode.TextDocument, _webview: vscode.Webview): void { + // Do nothing + } + protected update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { if (isDocumentChange) { return; diff --git a/src/extension/editors/digitaljs.ts b/src/extension/editors/yosys.ts similarity index 78% rename from src/extension/editors/digitaljs.ts rename to src/extension/editors/yosys.ts index 93ef2df..bd0f7db 100644 --- a/src/extension/editors/digitaljs.ts +++ b/src/extension/editors/yosys.ts @@ -1,11 +1,13 @@ import * as vscode from 'vscode'; -import {type ViewMessage} from '../types.js'; import * as util from '../util.js'; import {BaseEditor} from './base.js'; +import {type ViewMessage} from './messages.js'; + +export class YosysEditor extends BaseEditor { + private static activeViews: Set = new Set(); -export class DigitalJSEditor extends BaseEditor { public static getViewType() { return 'edacation.digitaljs'; } @@ -47,6 +49,13 @@ export class DigitalJSEditor extends BaseEditor { type: 'document', document: document.getText() }); + } else if (message.type === 'broadcast') { + for (const view of YosysEditor.activeViews) { + if (view === webview) { + continue; + } + view.postMessage(message); + } } else if (message.type === 'requestSave') { // Save to project root, or the parent dir of the current editor's file if we can't find it const projectRoot = util.findProjectRoot(document.uri) || util.getParentUri(document.uri); @@ -66,9 +75,18 @@ export class DigitalJSEditor extends BaseEditor { protected onSave(_document: vscode.TextDocument, _webview: vscode.Webview): void { // Do nothing + console.log(YosysEditor.activeViews); + } + + protected onClose(_document: vscode.TextDocument, webview: vscode.Webview): void { + console.log('Editor closed!'); + YosysEditor.activeViews.delete(webview); } protected update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { + console.log('View becomes active! (maybe)'); + YosysEditor.activeViews.add(webview); + if (!isDocumentChange) { vscode.commands.executeCommand('edacation-projects.focus'); } diff --git a/src/extension/tasks/rtl.ts b/src/extension/tasks/rtl.ts index 46107f6..0742eec 100644 --- a/src/extension/tasks/rtl.ts +++ b/src/extension/tasks/rtl.ts @@ -1,8 +1,10 @@ import {type YosysWorkerOptions, encodeText, generateYosysRTLCommands} from 'edacation'; +import {basename} from 'path-browserify'; import * as vscode from 'vscode'; import type {MessageFile} from '../../common/messages.js'; import type {Project} from '../projects/index.js'; +import {decodeJSON, encodeJSON} from '../util.js'; import { type WorkerOutputFile, @@ -42,12 +44,48 @@ class RTLTaskTerminal extends BaseYosysTaskTerminal { } protected getOutputFiles(_workerOptions: YosysWorkerOptions): string[] { - return ['rtl.digitaljs.json']; + return ['rtl.digitaljs.json', 'stats.digitaljs.json']; + } + + private async updateFile(uri: vscode.Uri) { + const fileName = basename(uri.path); + let fileType: string; + if (fileName === 'rtl.digitaljs.json') { + fileType = 'rtl'; + } else if (fileName === 'stats.digitaljs.json') { + fileType = 'stats'; + } else { + // TODO: print the message to stderr as a warning (red text). + // This is implemented in https://github.com/EDAcation/vscode-edacation/pull/14. + this.println( + `WARNING: Output file "${fileName}" not recognized. It might not be compatible with EDAcation.` + ); + return; + } + + // Actually update file + const oldContent = await vscode.workspace.fs.readFile(uri); + const newContent = encodeJSON({ + type: fileType, + data: decodeJSON(oldContent) + }); + await vscode.workspace.fs.writeFile(uri, newContent); } protected async handleEnd(project: Project, outputFiles: WorkerOutputFile[]) { super.handleEnd(project, outputFiles); + // Update file contents so our custom editor can handle them + // TODO: print the message to stderr as an error (red text). + // This is implemented in https://github.com/EDAcation/vscode-edacation/pull/14. + await Promise.all( + outputFiles.map((file) => + this.updateFile(file.uri).catch((_err) => + this.println(`Error while updating "${file.path}"; the file might not be usable!`) + ) + ) + ); + // Open RTL file in DigitalJS editor const rtlFile = outputFiles.find((file) => file.path.endsWith('rtl.digitaljs.json')); if (rtlFile) { diff --git a/src/extension/types.ts b/src/extension/types.ts deleted file mode 100644 index e31f678..0000000 --- a/src/extension/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface ViewMessageReady { - type: 'ready'; -} - -export interface ViewMessageChange { - type: 'change'; - document: string; -} - -export interface ViewMessageRequestSave { - type: 'requestSave'; - data: { - fileContents: string; - defaultPath?: string; - filters?: Record; - }; -} - -export type ViewMessage = ViewMessageReady | ViewMessageChange | ViewMessageRequestSave; diff --git a/views/digitaljs/src/main.ts b/views/digitaljs/src/main.ts index af1b654..d966c23 100644 --- a/views/digitaljs/src/main.ts +++ b/views/digitaljs/src/main.ts @@ -1,11 +1,11 @@ -import 'jquery-ui/dist/jquery-ui.min.js'; import '@vscode/codicons/dist/codicon.css'; import {allComponents} from '@vscode/webview-ui-toolkit/dist/toolkit.js'; -// @ts-expect-error: TODO: add module declaration (digitaljs.d.ts) -import {Circuit} from 'digitaljs'; -import {yosys2digitaljs} from 'yosys2digitaljs'; +import 'jquery-ui/dist/jquery-ui.min.js'; import './main.css'; +import type {EditorMessage, ForeignViewMessage, ViewMessage} from './messages'; +import * as viewers from './viewers'; +import type {BaseViewer} from './viewers/base'; import {vscode} from './vscode'; // Force bundler to include VS Code Webview UI Toolkit @@ -15,39 +15,16 @@ interface State { document?: string; } -interface MessageDocument { - type: 'document'; - document: string; -} - -type Message = MessageDocument; - -const getSvg = (svgElem: Element, width: number, height: number): string => { - // Filter conveniently labeled foreign objects from element - const foreignElems = svgElem.getElementsByTagName('foreignObject'); - for (const elem of Array.from(foreignElems)) { - elem.remove(); - } - - // Set correct XML namespace - svgElem.removeAttribute('xmlns:xlink'); - svgElem.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); - - // Correctly specify width / height to prevent clipping - svgElem.setAttribute('width', `${width}px`); - svgElem.setAttribute('height', `${height}px`); - - // Add XML header - return '\n' + svgElem.outerHTML; -}; +export class View { + public readonly root: HTMLDivElement; -class View { - private readonly root: HTMLDivElement; private state: State; + private viewer: BaseViewer | null; constructor(root: HTMLDivElement, state: State) { this.root = root; this.state = state; + this.viewer = null; addEventListener('message', this.handleMessage.bind(this)); addEventListener('messageerror', this.handleMessageError.bind(this)); @@ -56,13 +33,13 @@ class View { if (this.state.document) { this.renderDocument(); } else { - vscode.postMessage({ + this.sendMessage({ type: 'ready' }); } } - updateState(partialState: Partial) { + private updateState(partialState: Partial) { this.state = { ...this.state, ...partialState @@ -70,131 +47,95 @@ class View { vscode.setState(this.state); } - handleMessage(message: MessageEvent) { + private handleMessage(message: MessageEvent) { switch (message.data.type) { case 'document': { this.updateState({ document: message.data.document }); this.renderDocument(); + break; + } + case 'broadcast': { + if (this.viewer) { + this.viewer.handleForeignViewMessage(message.data.message); + } } } } - handleMessageError(message: MessageEvent) { - console.error(message); - this.handleError(new Error('Message error.')); + private handleMessageError(message: MessageEvent) { + this.handleError(`Message error\n\n${message}`); } - handleError(error: unknown) { - if (error instanceof Error || typeof error === 'string') { - this.renderError(error); - } else { - this.renderError(new Error('Unknown error.')); - } - } - - handleResize() { + private handleResize() { this.renderDocument(); } - requestExport() { - // Find our SVG root element - const svgElems = document.getElementsByTagName('svg'); - if (!svgElems) { - throw new Error('Could not find SVG element to export'); + private findViewer(): BaseViewer { + if (!this.state.document) { + throw new Error('No data to find viewer!'); } - const svgElem = svgElems[0]; - // Extract viewable SVG data from elem - const svgData = getSvg( - // Deep clone so we don't affect the SVG in the DOM - svgElem.cloneNode(true) as Element, - svgElem.clientWidth, - svgElem.clientHeight - ); + const fileData = JSON.parse(this.state.document); + const fileType = fileData['type']; + const viewerData = fileData['data']; + if (!fileType || !viewerData) { + throw new Error('File is missing type or data keys.'); + } - // Send save request to main worker - vscode.postMessage({ - type: 'requestSave', - data: { - fileContents: svgData, - defaultPath: 'export.svg', - saveFilters: {svg: ['.svg']} + for (const viewer of Object.values(viewers)) { + const viewerInst = new viewer(this, viewerData); + if (viewerInst.getType() === fileType) { + return viewerInst; } - }); + } + + throw new Error(`Could not find viewer for type: ${fileType}`); } - renderDocument() { + private renderDocument() { try { if (!this.state.document) { - throw new Error('No document to render.'); + throw new Error('No data to render document!'); + } else if (!this.viewer) { + this.viewer = this.findViewer(); } - // Parse Yosys netlist from JSON string - const json = JSON.parse(this.state.document); - - // Convert from Yosys netlist to DigitalJS format - const digitalJs = yosys2digitaljs(json); - - // Initialize circuit - const circuit = new Circuit(digitalJs); - - // Clear - this.root.replaceChildren(); - - // Render actions - const elementActions = document.createElement('div'); - elementActions.style.marginBottom = '1rem'; - elementActions.innerHTML = /*html*/ ` - - Start - - - - Stop - - - - Export to SVG - - - `; - this.root.appendChild(elementActions); - - const buttonStart = document.getElementById('digitaljs-start'); - const buttonStop = document.getElementById('digitaljs-stop'); - const buttonExport = document.getElementById('digitaljs-export'); - - buttonStart?.addEventListener('click', () => circuit.start()); - buttonStop?.addEventListener('click', () => circuit.stop()); - buttonExport?.addEventListener('click', this.requestExport); - - circuit.on('changeRunning', () => { - if (circuit.running) { - buttonStart?.setAttribute('disabled', ''); - buttonStop?.removeAttribute('disabled'); - } else { - buttonStart?.removeAttribute('disabled'); - buttonStop?.setAttribute('disabled', ''); - } - }); - - // Render circuit - const elementCircuit = document.createElement('div'); - circuit.displayOn(elementCircuit); - this.root.appendChild(elementCircuit); + this.viewer.render().catch((err) => this.handleError(err, this.viewer)); } catch (err) { this.handleError(err); } } - renderError(error: Error | string) { + sendMessage(message: ViewMessage) { + vscode.postMessage(message); + } + + broadcastMessage(message: ForeignViewMessage) { + vscode.postMessage({ + type: 'broadcast', + message: message + }); + } + + handleError(error: unknown, sourceViewer: BaseViewer | null = null) { + if (error instanceof Error || typeof error === 'string') { + this.renderError(error, sourceViewer); + } else { + this.renderError(new Error('Unknown error.'), sourceViewer); + } + } + + private renderError(error: Error | string, sourceViewer: BaseViewer | null) { const elementHeader = document.createElement('h3'); - elementHeader.textContent = 'Unable to render DigitalJS file'; + elementHeader.textContent = 'Unable to open DigitalJS file'; const elementCode = document.createElement('code'); - elementCode.textContent = typeof error === 'string' ? error : error.stack || error.message; + if (sourceViewer !== null) { + elementCode.textContent = `*** Error in Viewer: ${sourceViewer.getType()} ***\n\n`; + } + elementCode.textContent += typeof error === 'string' ? error : error.stack || error.message; this.root.replaceChildren(); this.root.appendChild(elementHeader); diff --git a/views/digitaljs/src/messages.ts b/views/digitaljs/src/messages.ts new file mode 100644 index 0000000..3446f9b --- /dev/null +++ b/views/digitaljs/src/messages.ts @@ -0,0 +1,38 @@ +interface ForeignViewMessageModuleFocus { + type: 'moduleFocus'; + module: string; +} + +export type ForeignViewMessage = ForeignViewMessageModuleFocus; + +interface MessageBroadcast { + type: 'broadcast'; + message: ForeignViewMessage; +} + +interface EditorMessageDocument { + type: 'document'; + document: string; +} + +export type EditorMessage = EditorMessageDocument | MessageBroadcast; + +interface ViewMessageReady { + type: 'ready'; +} + +interface ViewMessageChange { + type: 'change'; + document: string; +} + +interface ViewMessageRequestSave { + type: 'requestSave'; + data: { + fileContents: string; + defaultPath?: string; + filters?: Record; + }; +} + +export type ViewMessage = ViewMessageReady | ViewMessageChange | MessageBroadcast | ViewMessageRequestSave; diff --git a/views/digitaljs/src/viewers/base.ts b/views/digitaljs/src/viewers/base.ts new file mode 100644 index 0000000..f8f1baa --- /dev/null +++ b/views/digitaljs/src/viewers/base.ts @@ -0,0 +1,39 @@ +import type {View} from '../main'; +import type {ForeignViewMessage, ViewMessage} from '../messages'; + +export abstract class BaseViewer { + private readonly mainView: View; + protected readonly data: object; + + constructor(mainView: View, initData: object) { + this.mainView = mainView; + this.data = initData; + } + + // TODO: Should preferably be abstract as well... + abstract getType(): string; + + abstract render(): Promise; + + abstract handleForeignViewMessage(message: ForeignViewMessage): void; + + protected get root(): HTMLDivElement { + return this.mainView.root; + } + + protected broadcastMessage(message: ForeignViewMessage) { + return this.mainView.broadcastMessage(message); + } + + protected sendMessage(message: ViewMessage) { + return this.mainView.sendMessage(message); + } + + protected handleError(error: unknown) { + if (error instanceof Error || typeof error === 'string') { + this.mainView.handleError(error); + } else { + this.mainView.handleError(new Error('Unknown error.')); + } + } +} diff --git a/views/digitaljs/src/viewers/diagram.ts b/views/digitaljs/src/viewers/diagram.ts new file mode 100644 index 0000000..d8ed7e1 --- /dev/null +++ b/views/digitaljs/src/viewers/diagram.ts @@ -0,0 +1,119 @@ +// @ts-expect-error: TODO: add module declaration (digitaljs.d.ts) +import {Circuit} from 'digitaljs'; +import {yosys2digitaljs} from 'yosys2digitaljs'; + +import type {ForeignViewMessage} from '../messages'; + +import {BaseViewer} from './base'; + +type YosysOutput = Parameters[0]; + +const getSvg = (svgElem: Element, width: number, height: number): string => { + // Filter conveniently labeled foreign objects from element + const foreignElems = svgElem.getElementsByTagName('foreignObject'); + for (const elem of Array.from(foreignElems)) { + elem.remove(); + } + + // Set correct XML namespace + svgElem.removeAttribute('xmlns:xlink'); + svgElem.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + + // Correctly specify width / height to prevent clipping + svgElem.setAttribute('width', `${width}px`); + svgElem.setAttribute('height', `${height}px`); + + // Add XML header + return '\n' + svgElem.outerHTML; +}; + +export class DiagramViewer extends BaseViewer { + getType() { + return 'rtl'; + } + + handleForeignViewMessage(message: ForeignViewMessage): void { + console.log('Foreign message:'); + console.log(message); + } + + async render() { + // Convert from Yosys netlist to DigitalJS format + const digitalJs = yosys2digitaljs(this.data as YosysOutput); + + // Initialize circuit + const circuit = new Circuit(digitalJs); + + // Clear + this.root.replaceChildren(); + + // Render actions + const elementActions = document.createElement('div'); + elementActions.style.marginBottom = '1rem'; + elementActions.innerHTML = /*html*/ ` + + Start + + + + Stop + + + + Export to SVG + + + `; + this.root.appendChild(elementActions); + + const buttonStart = document.getElementById('digitaljs-start'); + const buttonStop = document.getElementById('digitaljs-stop'); + const buttonExport = document.getElementById('digitaljs-export'); + + buttonStart?.addEventListener('click', () => circuit.start()); + buttonStop?.addEventListener('click', () => circuit.stop()); + buttonExport?.addEventListener('click', this.requestExport.bind(this)); + + circuit.on('changeRunning', () => { + if (circuit.running) { + buttonStart?.setAttribute('disabled', ''); + buttonStop?.removeAttribute('disabled'); + } else { + buttonStart?.removeAttribute('disabled'); + buttonStop?.setAttribute('disabled', ''); + } + }); + + // Render circuit + const elementCircuit = document.createElement('div'); + circuit.displayOn(elementCircuit); + this.root.appendChild(elementCircuit); + } + + private requestExport() { + // Find our SVG root element + const svgElems = document.getElementsByTagName('svg'); + if (!svgElems) { + throw new Error('Could not find SVG element to export'); + } + const svgElem = svgElems[0]; + + // Extract viewable SVG data from elem + const svgData = getSvg( + // Deep clone so we don't affect the SVG in the DOM + svgElem.cloneNode(true) as Element, + svgElem.clientWidth, + svgElem.clientHeight + ); + + // Send save request to main worker + this.sendMessage({ + type: 'requestSave', + data: { + fileContents: svgData, + defaultPath: 'export.svg', + filters: {svg: ['.svg']} + } + }); + } +} diff --git a/views/digitaljs/src/viewers/index.ts b/views/digitaljs/src/viewers/index.ts new file mode 100644 index 0000000..308a338 --- /dev/null +++ b/views/digitaljs/src/viewers/index.ts @@ -0,0 +1,2 @@ +export * from './diagram'; +export * from './stats'; diff --git a/views/digitaljs/src/viewers/stats.ts b/views/digitaljs/src/viewers/stats.ts new file mode 100644 index 0000000..a6c1988 --- /dev/null +++ b/views/digitaljs/src/viewers/stats.ts @@ -0,0 +1,33 @@ +import type {ForeignViewMessage} from '../messages'; + +import {BaseViewer} from './base'; + +export class StatsViewer extends BaseViewer { + getType() { + return 'stats'; + } + + handleForeignViewMessage(message: ForeignViewMessage): void { + console.log('Foreign message:'); + console.log(message); + } + + async render(): Promise { + this.root.replaceChildren(); + + // TODO: make proper dropdowns etc + const textElem = document.createElement('p'); + textElem.textContent = JSON.stringify(this.data); + this.root.appendChild(textElem); + + const button = document.createElement('button'); + button.textContent = 'Click for broadcast'; + button.addEventListener('click', (_ev) => { + this.broadcastMessage({ + type: 'moduleFocus', + module: 'test_module_goes_here' + }); + }); + this.root.appendChild(button); + } +} From 83e96c0eb29d9b15f638a6a730d56a2317b77e08 Mon Sep 17 00:00:00 2001 From: Mike A Date: Mon, 21 Aug 2023 12:44:47 +0200 Subject: [PATCH 02/31] Fix webview tracking memory leak --- src/extension/editors/base.ts | 31 ++++++++++++------------------- src/extension/editors/yosys.ts | 2 -- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/extension/editors/base.ts b/src/extension/editors/base.ts index d7c38c8..a31879a 100644 --- a/src/extension/editors/base.ts +++ b/src/extension/editors/base.ts @@ -24,58 +24,51 @@ export abstract class BaseEditor implements vscode.CustomTextEditorProvider { _token: vscode.CancellationToken ): void | Thenable { const disposables: vscode.Disposable[] = []; + const webview = webviewPanel.webview; // Render webview - webviewPanel.webview.options = { + webview.options = { enableScripts: true }; - webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview, document); + webview.html = this.getHtmlForWebview(webview, document); // Add message listener - webviewPanel.webview.onDidReceiveMessage(this.onDidReceiveMessage.bind(this, document, webviewPanel.webview)); + webview.onDidReceiveMessage(this.onDidReceiveMessage.bind(this, document, webview)); // Add text document listener disposables.push( vscode.workspace.onDidChangeTextDocument((event) => { if (event.document.uri.toString() === document.uri.toString()) { - this.update(document, webviewPanel.webview, true); + this.update(document, webview, true); } }) ); disposables.push( vscode.workspace.onDidSaveTextDocument((event) => { if (event.uri.toString() === document.uri.toString()) { - this.onSave(document, webviewPanel.webview); - } - }) - ); - // TODO: does not work reliably! - disposables.push( - vscode.window.onDidChangeVisibleTextEditors((event) => { - console.log('New windows:'); - console.log(event); - if (event.map((editor) => editor.document).indexOf(document) !== -1) { - this.onClose(document, webviewPanel.webview); + this.onSave(document, webview); } }) ); // Create file system watcher const watcher = vscode.workspace.createFileSystemWatcher(document.uri.fsPath); - watcher.onDidCreate(() => this.update(document, webviewPanel.webview, true)); - watcher.onDidChange(() => this.update(document, webviewPanel.webview, true)); - watcher.onDidDelete(() => this.update(document, webviewPanel.webview, true)); + watcher.onDidCreate(() => this.update(document, webview, true)); + watcher.onDidChange(() => this.update(document, webview, true)); + watcher.onDidDelete(() => this.update(document, webview, true)); disposables.push(watcher); // Add dispose listener webviewPanel.onDidDispose(() => { + this.onClose(document, webview); + for (const disposable of disposables) { disposable.dispose(); } }); // Update document - this.update(document, webviewPanel.webview, false); + this.update(document, webview, false); } protected getHtmlForWebview(webview: vscode.Webview, document: vscode.TextDocument): string { diff --git a/src/extension/editors/yosys.ts b/src/extension/editors/yosys.ts index bd0f7db..b7bcf9b 100644 --- a/src/extension/editors/yosys.ts +++ b/src/extension/editors/yosys.ts @@ -79,12 +79,10 @@ export class YosysEditor extends BaseEditor { } protected onClose(_document: vscode.TextDocument, webview: vscode.Webview): void { - console.log('Editor closed!'); YosysEditor.activeViews.delete(webview); } protected update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { - console.log('View becomes active! (maybe)'); YosysEditor.activeViews.add(webview); if (!isDocumentChange) { From 6a224fec7d2625f7bd9d13c4ed97f8985194532f Mon Sep 17 00:00:00 2001 From: Mike A Date: Mon, 21 Aug 2023 13:58:41 +0200 Subject: [PATCH 03/31] Improve typing --- src/extension/editors/yosys.ts | 1 - views/digitaljs/src/main.ts | 33 ++++++++++++-------------- views/digitaljs/src/types.ts | 33 ++++++++++++++++++++++++++ views/digitaljs/src/viewers/base.ts | 9 +++---- views/digitaljs/src/viewers/diagram.ts | 11 +++------ views/digitaljs/src/viewers/index.ts | 1 + views/digitaljs/src/viewers/stats.ts | 7 ++---- 7 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 views/digitaljs/src/types.ts diff --git a/src/extension/editors/yosys.ts b/src/extension/editors/yosys.ts index b7bcf9b..d005622 100644 --- a/src/extension/editors/yosys.ts +++ b/src/extension/editors/yosys.ts @@ -75,7 +75,6 @@ export class YosysEditor extends BaseEditor { protected onSave(_document: vscode.TextDocument, _webview: vscode.Webview): void { // Do nothing - console.log(YosysEditor.activeViews); } protected onClose(_document: vscode.TextDocument, webview: vscode.Webview): void { diff --git a/views/digitaljs/src/main.ts b/views/digitaljs/src/main.ts index d966c23..9062f63 100644 --- a/views/digitaljs/src/main.ts +++ b/views/digitaljs/src/main.ts @@ -4,8 +4,8 @@ import 'jquery-ui/dist/jquery-ui.min.js'; import './main.css'; import type {EditorMessage, ForeignViewMessage, ViewMessage} from './messages'; -import * as viewers from './viewers'; -import type {BaseViewer} from './viewers/base'; +import type {YosysFile} from './types'; +import {type BaseViewer, DiagramViewer, StatsViewer} from './viewers'; import {vscode} from './vscode'; // Force bundler to include VS Code Webview UI Toolkit @@ -19,7 +19,7 @@ export class View { public readonly root: HTMLDivElement; private state: State; - private viewer: BaseViewer | null; + private viewer: BaseViewer | null; constructor(root: HTMLDivElement, state: State) { this.root = root; @@ -72,26 +72,23 @@ export class View { this.renderDocument(); } - private findViewer(): BaseViewer { + private findViewer(): BaseViewer { if (!this.state.document) { throw new Error('No data to find viewer!'); } - const fileData = JSON.parse(this.state.document); - const fileType = fileData['type']; - const viewerData = fileData['data']; - if (!fileType || !viewerData) { + const fileData = JSON.parse(this.state.document) as YosysFile; + if (!fileData['type'] || !fileData['data']) { throw new Error('File is missing type or data keys.'); } - for (const viewer of Object.values(viewers)) { - const viewerInst = new viewer(this, viewerData); - if (viewerInst.getType() === fileType) { - return viewerInst; - } + if (fileData['type'] === 'rtl') { + return new DiagramViewer(this, fileData['data']); + } else if (fileData['type'] === 'stats') { + return new StatsViewer(this, fileData['data']); + } else { + throw new Error(`Could not find viewer for type: ${fileData['type']}`); } - - throw new Error(`Could not find viewer for type: ${fileType}`); } private renderDocument() { @@ -119,7 +116,7 @@ export class View { }); } - handleError(error: unknown, sourceViewer: BaseViewer | null = null) { + handleError(error: unknown, sourceViewer: BaseViewer | null = null) { if (error instanceof Error || typeof error === 'string') { this.renderError(error, sourceViewer); } else { @@ -127,13 +124,13 @@ export class View { } } - private renderError(error: Error | string, sourceViewer: BaseViewer | null) { + private renderError(error: Error | string, sourceViewer: BaseViewer | null) { const elementHeader = document.createElement('h3'); elementHeader.textContent = 'Unable to open DigitalJS file'; const elementCode = document.createElement('code'); if (sourceViewer !== null) { - elementCode.textContent = `*** Error in Viewer: ${sourceViewer.getType()} ***\n\n`; + elementCode.textContent = `*** Error in Viewer: ${typeof sourceViewer} ***\n\n`; } elementCode.textContent += typeof error === 'string' ? error : error.stack || error.message; diff --git a/views/digitaljs/src/types.ts b/views/digitaljs/src/types.ts new file mode 100644 index 0000000..920e711 --- /dev/null +++ b/views/digitaljs/src/types.ts @@ -0,0 +1,33 @@ +import type {yosys2digitaljs} from 'yosys2digitaljs'; + +export type YosysRTL = Parameters[0]; + +interface YosysFileRTL { + type: 'rtl'; + data: YosysRTL; +} + +interface YosysModuleStats { + num_wires: number; + num_wire_bits: number; + num_pub_wires: number; + num_pub_wire_bits: number; + num_memories: number; + num_memory_bits: number; + num_processes: number; + num_cells: number; + num_cells_by_type: Record; +} + +export interface YosysStats { + creator: string; + invocation: string; + modules: Record; +} + +interface YosysFileStats { + type: 'stats'; + data: YosysStats; +} + +export type YosysFile = YosysFileRTL | YosysFileStats; diff --git a/views/digitaljs/src/viewers/base.ts b/views/digitaljs/src/viewers/base.ts index f8f1baa..db2ef3a 100644 --- a/views/digitaljs/src/viewers/base.ts +++ b/views/digitaljs/src/viewers/base.ts @@ -1,18 +1,15 @@ import type {View} from '../main'; import type {ForeignViewMessage, ViewMessage} from '../messages'; -export abstract class BaseViewer { +export abstract class BaseViewer { private readonly mainView: View; - protected readonly data: object; + protected readonly data: InitialData; - constructor(mainView: View, initData: object) { + constructor(mainView: View, initData: InitialData) { this.mainView = mainView; this.data = initData; } - // TODO: Should preferably be abstract as well... - abstract getType(): string; - abstract render(): Promise; abstract handleForeignViewMessage(message: ForeignViewMessage): void; diff --git a/views/digitaljs/src/viewers/diagram.ts b/views/digitaljs/src/viewers/diagram.ts index d8ed7e1..be4e07b 100644 --- a/views/digitaljs/src/viewers/diagram.ts +++ b/views/digitaljs/src/viewers/diagram.ts @@ -3,11 +3,10 @@ import {Circuit} from 'digitaljs'; import {yosys2digitaljs} from 'yosys2digitaljs'; import type {ForeignViewMessage} from '../messages'; +import type {YosysRTL} from '../types'; import {BaseViewer} from './base'; -type YosysOutput = Parameters[0]; - const getSvg = (svgElem: Element, width: number, height: number): string => { // Filter conveniently labeled foreign objects from element const foreignElems = svgElem.getElementsByTagName('foreignObject'); @@ -27,11 +26,7 @@ const getSvg = (svgElem: Element, width: number, height: number): string => { return '\n' + svgElem.outerHTML; }; -export class DiagramViewer extends BaseViewer { - getType() { - return 'rtl'; - } - +export class DiagramViewer extends BaseViewer { handleForeignViewMessage(message: ForeignViewMessage): void { console.log('Foreign message:'); console.log(message); @@ -39,7 +34,7 @@ export class DiagramViewer extends BaseViewer { async render() { // Convert from Yosys netlist to DigitalJS format - const digitalJs = yosys2digitaljs(this.data as YosysOutput); + const digitalJs = yosys2digitaljs(this.data); // Initialize circuit const circuit = new Circuit(digitalJs); diff --git a/views/digitaljs/src/viewers/index.ts b/views/digitaljs/src/viewers/index.ts index 308a338..f10e1b4 100644 --- a/views/digitaljs/src/viewers/index.ts +++ b/views/digitaljs/src/viewers/index.ts @@ -1,2 +1,3 @@ export * from './diagram'; export * from './stats'; +export * from './base'; diff --git a/views/digitaljs/src/viewers/stats.ts b/views/digitaljs/src/viewers/stats.ts index a6c1988..dd69107 100644 --- a/views/digitaljs/src/viewers/stats.ts +++ b/views/digitaljs/src/viewers/stats.ts @@ -1,12 +1,9 @@ import type {ForeignViewMessage} from '../messages'; +import type {YosysStats} from '../types'; import {BaseViewer} from './base'; -export class StatsViewer extends BaseViewer { - getType() { - return 'stats'; - } - +export class StatsViewer extends BaseViewer { handleForeignViewMessage(message: ForeignViewMessage): void { console.log('Foreign message:'); console.log(message); From 095860896cb81f3c7379ef5d8f88019088dcecd6 Mon Sep 17 00:00:00 2001 From: Mike A Date: Tue, 22 Aug 2023 17:13:08 +0200 Subject: [PATCH 04/31] Improve statistics viewer --- views/digitaljs/src/types.ts | 2 +- views/digitaljs/src/viewers/stats.ts | 292 ++++++++++++++++++++++++++- 2 files changed, 288 insertions(+), 6 deletions(-) diff --git a/views/digitaljs/src/types.ts b/views/digitaljs/src/types.ts index 920e711..8b21771 100644 --- a/views/digitaljs/src/types.ts +++ b/views/digitaljs/src/types.ts @@ -7,7 +7,7 @@ interface YosysFileRTL { data: YosysRTL; } -interface YosysModuleStats { +export interface YosysModuleStats { num_wires: number; num_wire_bits: number; num_pub_wires: number; diff --git a/views/digitaljs/src/viewers/stats.ts b/views/digitaljs/src/viewers/stats.ts index dd69107..86bee8d 100644 --- a/views/digitaljs/src/viewers/stats.ts +++ b/views/digitaljs/src/viewers/stats.ts @@ -1,22 +1,304 @@ +// import type {DataGrid} from '@vscode/webview-ui-toolkit'; +import type {View} from '../main'; import type {ForeignViewMessage} from '../messages'; -import type {YosysStats} from '../types'; +import type {YosysModuleStats, YosysStats} from '../types'; import {BaseViewer} from './base'; +type DataGridRow = (Node | string)[]; + +class DataGrid { + private rootElem: HTMLElement; + + constructor(headers: string[] = []) { + this.rootElem = document.createElement('vscode-data-grid'); + + this.setHeaders(headers); + } + + get element(): HTMLElement { + return this.rootElem; + } + + setHeaders(headers: string[]) { + let headerElem = this.rootElem.children.item(0); + if (!headerElem) { + headerElem = document.createElement('vscode-data-grid-row'); + headerElem.setAttribute('row-type', 'header'); + this.rootElem.appendChild(headerElem); + } + + headerElem.replaceChildren(); + for (let i = 0; i < headers.length; i++) { + const cell = document.createElement('vscode-data-grid-cell'); + cell.setAttribute('cell-type', 'columnheader'); + cell.setAttribute('grid-column', (i + 1).toString()); + cell.innerText = headers[i]; + headerElem.appendChild(cell); + } + } + + clearRows() { + for (const child of Array.from(this.rootElem.children).slice(1)) { + child.remove(); + } + } + + addRows(rows: DataGridRow[]) { + for (const row of rows) { + this.addRow(row); + } + } + + addRow(cells: DataGridRow) { + const row = document.createElement('vscode-data-grid-row'); + for (let i = 0; i < cells.length; i++) { + const cell = document.createElement('vscode-data-grid-cell'); + cell.setAttribute('grid-column', (i + 1).toString()); + cell.append(cells[i]); + row.appendChild(cell); + } + this.rootElem.appendChild(row); + } +} + +interface ModuleStats { + [index: string]: number; + memoryCount: number; + memoryBitCount: number; + processCount: number; + cellCount: number; +} + +class Module { + private _children: Map; + private parents: Set; + private primitiveModules: Map; + + public readonly name: string; + public readonly localStats: ModuleStats; + private _globalStats: ModuleStats | null; + + constructor(name: string, stats: YosysModuleStats) { + this._children = new Map(); + this.parents = new Set(); + this.primitiveModules = new Map(); + + this.name = name; + this.localStats = { + memoryCount: stats.num_memories, + memoryBitCount: stats.num_memory_bits, + processCount: stats.num_processes, + cellCount: stats.num_cells + }; + this._globalStats = null; + } + + get primitives(): Map { + return this.primitiveModules; + } + + get children(): Map { + return this._children; + } + + get isTopLevel(): boolean { + return this.parents.size === 0; + } + + get globalStats(): ModuleStats { + if (this._globalStats) { + return this._globalStats; + } + + this._globalStats = structuredClone(this.localStats); + for (const [module, count] of this._children) { + const moduleStats = module.globalStats; + this._globalStats.memoryCount += count * moduleStats.memoryCount; + this._globalStats.memoryBitCount += count * moduleStats.memoryBitCount; + this._globalStats.cellCount += count * moduleStats.cellCount; + } + + return this._globalStats; + } + + addPrimitive(prim: string, count: number) { + const curCount = this.primitiveModules.get(prim) || 0; + this.primitiveModules.set(prim, curCount + count); + } + + addChild(module: Module, count: number) { + const curCount = this._children.get(module) || 0; + this._children.set(module, curCount + count); + module.addParent(this); + } + + protected addParent(module: Module) { + this.parents.add(module); + } +} + +const resolveModuleDeps = (name: string, stats: YosysModuleStats, resolved: Map): Module | null => { + const module = new Module(name, stats); + for (const [depName, count] of Object.entries(stats.num_cells_by_type)) { + if (depName.startsWith('$')) { + module.addPrimitive(depName.slice(1), count); + continue; + } + + const childDep = resolved.get(depName); + if (!childDep) { + // Cannot fully be resolved yet + return null; + } + module.addChild(childDep, count); + } + + return module; +}; + +const buildModuleTree = (modules: Record): Module[] => { + const modEntries = Object.entries(modules); + const modCount = modEntries.length; + if (!modCount) { + return []; + } + + const resolved = new Map(); + while (resolved.size < modCount) { + const oldSize = resolved.size; + for (let i = modEntries.length - 1; i >= 0; i--) { + const [modName, stats] = modEntries[i]; + const sanitizedName = modName.slice(1); // Strip leading "\" + if (sanitizedName in resolved.keys()) { + continue; + } + + const module = resolveModuleDeps(sanitizedName, stats, resolved); + if (!module) { + continue; + } + resolved.set(sanitizedName, module); + modEntries.splice(i, 1); + } + if (resolved.size <= oldSize) { + throw new Error('Cyclic dependency loop detected; cannot resolve module dependency tree!'); + } + } + + return Array.from(resolved.values()).reverse(); +}; + export class StatsViewer extends BaseViewer { handleForeignViewMessage(message: ForeignViewMessage): void { console.log('Foreign message:'); console.log(message); } + private modules: Module[]; + + private overviewGrid: DataGrid; + + private explorerHistory: Module[]; + private explorerBreadcrumbs: HTMLParagraphElement; + private explorerGrid: DataGrid; + + constructor(mainView: View, initData: YosysStats) { + super(mainView, initData); + + this.modules = buildModuleTree(this.data.modules); + if (!this.modules) { + throw new Error('No circuit modules found to display!'); + } + + this.overviewGrid = new DataGrid(['Module name', 'Memories / Bits', 'Processes', 'Cells']); + + this.explorerHistory = [this.modules[0]]; + this.explorerBreadcrumbs = document.createElement('p'); + this.explorerGrid = new DataGrid(['Module name', 'Count', 'Cells local / total']); + } + + private renderExplorer() { + // Breadcrumbs header + this.explorerBreadcrumbs.replaceChildren(); + for (let i = 0; i < this.explorerHistory.length; i++) { + const link = document.createElement('vscode-link'); + link.textContent = this.explorerHistory[i].name; + link.addEventListener('click', (_ev) => { + this.explorerHistory = this.explorerHistory.slice(0, i + 1); + this.renderExplorer(); + }); + this.explorerBreadcrumbs.append(link); + + if (i != this.explorerHistory.length - 1) { + this.explorerBreadcrumbs.append(' > '); + } + } + + // Explorer panel + const target = this.explorerHistory[this.explorerHistory.length - 1]; + const fmtStat = (stat1: number, stat2: number) => `${stat1}/${stat2} (${(stat1 / stat2) * 100}%)`; + + this.explorerGrid.clearRows(); + this.explorerGrid.addRow([ + '', + '-', + fmtStat(target.localStats.cellCount, target.globalStats.cellCount) + ]); + for (const [prim, count] of target.primitives.entries()) { + this.explorerGrid.addRow([`$${prim}`, count.toString(), '-']); + } + for (const [child, count] of target.children.entries()) { + const link = document.createElement('vscode-link'); + link.textContent = child.name; + link.addEventListener('click', (_ev) => { + this.explorerHistory.push(child); + this.renderExplorer(); + }); + + this.explorerGrid.addRow([ + link, + count.toString(), + fmtStat(child.globalStats.cellCount * count, target.globalStats.cellCount) + ]); + } + } + async render(): Promise { this.root.replaceChildren(); - // TODO: make proper dropdowns etc - const textElem = document.createElement('p'); - textElem.textContent = JSON.stringify(this.data); - this.root.appendChild(textElem); + // ** Overview Table ** + const overviewHeader = document.createElement('h2'); + overviewHeader.textContent = 'Circuit overview'; + this.root.appendChild(overviewHeader); + + this.overviewGrid.clearRows(); + this.overviewGrid.addRows( + this.modules.map((module) => { + const gloStat = module.globalStats; + return [ + module.name + (module.isTopLevel ? ' (top-level)' : ''), + `${gloStat.memoryCount} / ${gloStat.memoryBitCount}`, + gloStat.processCount.toString(), + gloStat.cellCount.toString() + ]; + }) + ); + this.root.appendChild(this.overviewGrid.element); + + this.root.appendChild(document.createElement('br')); + this.root.appendChild(document.createElement('vscode-divider')); + + // ** Circuit explorer ** + const explorerHeader = document.createElement('h2'); + explorerHeader.textContent = 'Circuit explorer'; + this.root.appendChild(explorerHeader); + + this.renderExplorer(); + this.root.appendChild(this.explorerBreadcrumbs); + this.root.appendChild(this.explorerGrid.element); + // ** Other stuff ** const button = document.createElement('button'); button.textContent = 'Click for broadcast'; button.addEventListener('click', (_ev) => { From 65be7a851f0fffa6e0a20e8eccbd363090dfaf3f Mon Sep 17 00:00:00 2001 From: Mike A Date: Wed, 30 Aug 2023 15:30:09 +0200 Subject: [PATCH 05/31] Refactor + custom module overview headers --- .../src/viewers/{ => diagram}/diagram.ts | 7 +- views/digitaljs/src/viewers/index.ts | 4 +- views/digitaljs/src/viewers/stats.ts | 312 ---------------- views/digitaljs/src/viewers/stats/elements.ts | 339 ++++++++++++++++++ views/digitaljs/src/viewers/stats/events.ts | 28 ++ views/digitaljs/src/viewers/stats/modules.ts | 148 ++++++++ views/digitaljs/src/viewers/stats/stats.ts | 96 +++++ 7 files changed, 616 insertions(+), 318 deletions(-) rename views/digitaljs/src/viewers/{ => diagram}/diagram.ts (96%) delete mode 100644 views/digitaljs/src/viewers/stats.ts create mode 100644 views/digitaljs/src/viewers/stats/elements.ts create mode 100644 views/digitaljs/src/viewers/stats/events.ts create mode 100644 views/digitaljs/src/viewers/stats/modules.ts create mode 100644 views/digitaljs/src/viewers/stats/stats.ts diff --git a/views/digitaljs/src/viewers/diagram.ts b/views/digitaljs/src/viewers/diagram/diagram.ts similarity index 96% rename from views/digitaljs/src/viewers/diagram.ts rename to views/digitaljs/src/viewers/diagram/diagram.ts index be4e07b..9950b7f 100644 --- a/views/digitaljs/src/viewers/diagram.ts +++ b/views/digitaljs/src/viewers/diagram/diagram.ts @@ -2,10 +2,9 @@ import {Circuit} from 'digitaljs'; import {yosys2digitaljs} from 'yosys2digitaljs'; -import type {ForeignViewMessage} from '../messages'; -import type {YosysRTL} from '../types'; - -import {BaseViewer} from './base'; +import type {ForeignViewMessage} from '../../messages'; +import type {YosysRTL} from '../../types'; +import {BaseViewer} from '../base'; const getSvg = (svgElem: Element, width: number, height: number): string => { // Filter conveniently labeled foreign objects from element diff --git a/views/digitaljs/src/viewers/index.ts b/views/digitaljs/src/viewers/index.ts index f10e1b4..79cf11f 100644 --- a/views/digitaljs/src/viewers/index.ts +++ b/views/digitaljs/src/viewers/index.ts @@ -1,3 +1,3 @@ -export * from './diagram'; -export * from './stats'; +export * from './diagram/diagram'; +export * from './stats/stats'; export * from './base'; diff --git a/views/digitaljs/src/viewers/stats.ts b/views/digitaljs/src/viewers/stats.ts deleted file mode 100644 index 86bee8d..0000000 --- a/views/digitaljs/src/viewers/stats.ts +++ /dev/null @@ -1,312 +0,0 @@ -// import type {DataGrid} from '@vscode/webview-ui-toolkit'; -import type {View} from '../main'; -import type {ForeignViewMessage} from '../messages'; -import type {YosysModuleStats, YosysStats} from '../types'; - -import {BaseViewer} from './base'; - -type DataGridRow = (Node | string)[]; - -class DataGrid { - private rootElem: HTMLElement; - - constructor(headers: string[] = []) { - this.rootElem = document.createElement('vscode-data-grid'); - - this.setHeaders(headers); - } - - get element(): HTMLElement { - return this.rootElem; - } - - setHeaders(headers: string[]) { - let headerElem = this.rootElem.children.item(0); - if (!headerElem) { - headerElem = document.createElement('vscode-data-grid-row'); - headerElem.setAttribute('row-type', 'header'); - this.rootElem.appendChild(headerElem); - } - - headerElem.replaceChildren(); - for (let i = 0; i < headers.length; i++) { - const cell = document.createElement('vscode-data-grid-cell'); - cell.setAttribute('cell-type', 'columnheader'); - cell.setAttribute('grid-column', (i + 1).toString()); - cell.innerText = headers[i]; - headerElem.appendChild(cell); - } - } - - clearRows() { - for (const child of Array.from(this.rootElem.children).slice(1)) { - child.remove(); - } - } - - addRows(rows: DataGridRow[]) { - for (const row of rows) { - this.addRow(row); - } - } - - addRow(cells: DataGridRow) { - const row = document.createElement('vscode-data-grid-row'); - for (let i = 0; i < cells.length; i++) { - const cell = document.createElement('vscode-data-grid-cell'); - cell.setAttribute('grid-column', (i + 1).toString()); - cell.append(cells[i]); - row.appendChild(cell); - } - this.rootElem.appendChild(row); - } -} - -interface ModuleStats { - [index: string]: number; - memoryCount: number; - memoryBitCount: number; - processCount: number; - cellCount: number; -} - -class Module { - private _children: Map; - private parents: Set; - private primitiveModules: Map; - - public readonly name: string; - public readonly localStats: ModuleStats; - private _globalStats: ModuleStats | null; - - constructor(name: string, stats: YosysModuleStats) { - this._children = new Map(); - this.parents = new Set(); - this.primitiveModules = new Map(); - - this.name = name; - this.localStats = { - memoryCount: stats.num_memories, - memoryBitCount: stats.num_memory_bits, - processCount: stats.num_processes, - cellCount: stats.num_cells - }; - this._globalStats = null; - } - - get primitives(): Map { - return this.primitiveModules; - } - - get children(): Map { - return this._children; - } - - get isTopLevel(): boolean { - return this.parents.size === 0; - } - - get globalStats(): ModuleStats { - if (this._globalStats) { - return this._globalStats; - } - - this._globalStats = structuredClone(this.localStats); - for (const [module, count] of this._children) { - const moduleStats = module.globalStats; - this._globalStats.memoryCount += count * moduleStats.memoryCount; - this._globalStats.memoryBitCount += count * moduleStats.memoryBitCount; - this._globalStats.cellCount += count * moduleStats.cellCount; - } - - return this._globalStats; - } - - addPrimitive(prim: string, count: number) { - const curCount = this.primitiveModules.get(prim) || 0; - this.primitiveModules.set(prim, curCount + count); - } - - addChild(module: Module, count: number) { - const curCount = this._children.get(module) || 0; - this._children.set(module, curCount + count); - module.addParent(this); - } - - protected addParent(module: Module) { - this.parents.add(module); - } -} - -const resolveModuleDeps = (name: string, stats: YosysModuleStats, resolved: Map): Module | null => { - const module = new Module(name, stats); - for (const [depName, count] of Object.entries(stats.num_cells_by_type)) { - if (depName.startsWith('$')) { - module.addPrimitive(depName.slice(1), count); - continue; - } - - const childDep = resolved.get(depName); - if (!childDep) { - // Cannot fully be resolved yet - return null; - } - module.addChild(childDep, count); - } - - return module; -}; - -const buildModuleTree = (modules: Record): Module[] => { - const modEntries = Object.entries(modules); - const modCount = modEntries.length; - if (!modCount) { - return []; - } - - const resolved = new Map(); - while (resolved.size < modCount) { - const oldSize = resolved.size; - for (let i = modEntries.length - 1; i >= 0; i--) { - const [modName, stats] = modEntries[i]; - const sanitizedName = modName.slice(1); // Strip leading "\" - if (sanitizedName in resolved.keys()) { - continue; - } - - const module = resolveModuleDeps(sanitizedName, stats, resolved); - if (!module) { - continue; - } - resolved.set(sanitizedName, module); - modEntries.splice(i, 1); - } - if (resolved.size <= oldSize) { - throw new Error('Cyclic dependency loop detected; cannot resolve module dependency tree!'); - } - } - - return Array.from(resolved.values()).reverse(); -}; - -export class StatsViewer extends BaseViewer { - handleForeignViewMessage(message: ForeignViewMessage): void { - console.log('Foreign message:'); - console.log(message); - } - - private modules: Module[]; - - private overviewGrid: DataGrid; - - private explorerHistory: Module[]; - private explorerBreadcrumbs: HTMLParagraphElement; - private explorerGrid: DataGrid; - - constructor(mainView: View, initData: YosysStats) { - super(mainView, initData); - - this.modules = buildModuleTree(this.data.modules); - if (!this.modules) { - throw new Error('No circuit modules found to display!'); - } - - this.overviewGrid = new DataGrid(['Module name', 'Memories / Bits', 'Processes', 'Cells']); - - this.explorerHistory = [this.modules[0]]; - this.explorerBreadcrumbs = document.createElement('p'); - this.explorerGrid = new DataGrid(['Module name', 'Count', 'Cells local / total']); - } - - private renderExplorer() { - // Breadcrumbs header - this.explorerBreadcrumbs.replaceChildren(); - for (let i = 0; i < this.explorerHistory.length; i++) { - const link = document.createElement('vscode-link'); - link.textContent = this.explorerHistory[i].name; - link.addEventListener('click', (_ev) => { - this.explorerHistory = this.explorerHistory.slice(0, i + 1); - this.renderExplorer(); - }); - this.explorerBreadcrumbs.append(link); - - if (i != this.explorerHistory.length - 1) { - this.explorerBreadcrumbs.append(' > '); - } - } - - // Explorer panel - const target = this.explorerHistory[this.explorerHistory.length - 1]; - const fmtStat = (stat1: number, stat2: number) => `${stat1}/${stat2} (${(stat1 / stat2) * 100}%)`; - - this.explorerGrid.clearRows(); - this.explorerGrid.addRow([ - '', - '-', - fmtStat(target.localStats.cellCount, target.globalStats.cellCount) - ]); - for (const [prim, count] of target.primitives.entries()) { - this.explorerGrid.addRow([`$${prim}`, count.toString(), '-']); - } - for (const [child, count] of target.children.entries()) { - const link = document.createElement('vscode-link'); - link.textContent = child.name; - link.addEventListener('click', (_ev) => { - this.explorerHistory.push(child); - this.renderExplorer(); - }); - - this.explorerGrid.addRow([ - link, - count.toString(), - fmtStat(child.globalStats.cellCount * count, target.globalStats.cellCount) - ]); - } - } - - async render(): Promise { - this.root.replaceChildren(); - - // ** Overview Table ** - const overviewHeader = document.createElement('h2'); - overviewHeader.textContent = 'Circuit overview'; - this.root.appendChild(overviewHeader); - - this.overviewGrid.clearRows(); - this.overviewGrid.addRows( - this.modules.map((module) => { - const gloStat = module.globalStats; - return [ - module.name + (module.isTopLevel ? ' (top-level)' : ''), - `${gloStat.memoryCount} / ${gloStat.memoryBitCount}`, - gloStat.processCount.toString(), - gloStat.cellCount.toString() - ]; - }) - ); - this.root.appendChild(this.overviewGrid.element); - - this.root.appendChild(document.createElement('br')); - this.root.appendChild(document.createElement('vscode-divider')); - - // ** Circuit explorer ** - const explorerHeader = document.createElement('h2'); - explorerHeader.textContent = 'Circuit explorer'; - this.root.appendChild(explorerHeader); - - this.renderExplorer(); - this.root.appendChild(this.explorerBreadcrumbs); - this.root.appendChild(this.explorerGrid.element); - - // ** Other stuff ** - const button = document.createElement('button'); - button.textContent = 'Click for broadcast'; - button.addEventListener('click', (_ev) => { - this.broadcastMessage({ - type: 'moduleFocus', - module: 'test_module_goes_here' - }); - }); - this.root.appendChild(button); - } -} diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts new file mode 100644 index 0000000..46ad269 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -0,0 +1,339 @@ +import type {CustomEventListener, CustomEvents} from './events'; +import {type Module, ModuleStatId, getModuleStatIds, getModuleStatName} from './modules'; + +const fmtStat = (stat1: number, stat2: number): string => { + let res = `${stat1}/${stat2}`; + if (stat1 !== 0) { + res += ` (${(stat1 / stat2) * 100}%)`; + } + + return res; +}; + +export abstract class CustomElement { + protected abstract rootElem: HTMLElement; + + addEventListener(type: K, listener: CustomEventListener) { + this.element.addEventListener(type, (ev: CustomEventInit) => listener(ev.detail)); + } + + protected dispatchEvent(type: K, data: CustomEvents[K]) { + this.element.dispatchEvent(new CustomEvent(type, {detail: data})); + } + + get element() { + return this.rootElem; + } + + abstract render(): void; +} + +type DataGridCell = Node | string; + +export class DataGrid extends CustomElement { + protected rootElem: HTMLElement; + + protected cells: DataGridCell[][]; + + constructor(headers: string[] = []) { + super(); + + this.cells = [headers]; + this.rootElem = document.createElement('vscode-data-grid'); + } + + get width() { + return this.cells[0].length; + } + + get height() { + return this.cells.length; + } + + addColumn(contents: DataGridCell[], pos?: number): number { + contents = contents.slice(0, this.height); + if (contents.length < this.height) { + contents = contents.concat(new Array(this.height - contents.length)); + } + + if (pos === undefined) { + pos = this.width; + } + + for (let i = 0; i < this.height; i++) { + this.cells[i].splice(pos, 0, contents[i]); + } + + return pos; + } + + addRow(contents: DataGridCell[], pos?: number): number { + contents = contents.slice(0, this.width); + if (contents.length < this.width) { + contents = contents.concat(new Array(this.height - contents.length)); + } + + if (pos === undefined) { + pos = this.height - 1; + } + + this.cells.splice(pos + 1, 0, contents); + + return pos; + } + + setCell(x: number, y: number, value: DataGridCell) { + this.cells[y][x] = value; + + const row = this.rootElem.querySelectorAll('vscode-data-grid-row')[y]; + if (!row) { + return; + } + const cell = row.querySelectorAll('vscode-data-grid-cell')[x]; + if (!cell) { + return; + } + cell.replaceChildren(); + cell.append(value); + } + + clearRows() { + this.cells.splice(1); + } + + render() { + const newRoot = document.createElement('vscode-data-grid'); + this.rootElem.replaceWith(newRoot); + this.rootElem = newRoot; + + // Headers + const headerElem = document.createElement('vscode-data-grid-row'); + headerElem.setAttribute('row-type', 'header'); + + for (let i = 0; i < this.width; i++) { + const cell = document.createElement('vscode-data-grid-cell'); + cell.setAttribute('cell-type', 'columnheader'); + cell.setAttribute('grid-column', (i + 1).toString()); + cell.append(this.cells[0][i]); + headerElem.appendChild(cell); + } + this.rootElem.appendChild(headerElem); + + // Rows + for (let rowi = 1; rowi < this.height; rowi++) { + const row = document.createElement('vscode-data-grid-row'); + + for (let coli = 0; coli < this.width; coli++) { + const cell = document.createElement('vscode-data-grid-cell'); + cell.setAttribute('grid-column', (coli + 1).toString()); + cell.append(this.cells[rowi][coli]); + row.appendChild(cell); + } + this.rootElem.appendChild(row); + } + } +} + +export class ModuleOverviewGrid extends DataGrid { + private moduleStats: ModuleStatId[]; + + private readonly modules: Module[]; + + constructor(modules: Module[]) { + super(); + + this.modules = modules; + this.moduleStats = []; + + super.addColumn(['Module Name']); + super.addColumn(['Count']); + } + + private fillCell(x: number, y: number, statId?: ModuleStatId) { + if (x === 0 || y === 0) { + throw new Error('Cannot fill header cells!'); + } + + if (x === 1) { + // TODO: add module count + return super.setCell(x, y, '-'); + } + + if (!statId) { + // Find active column stat id + const dropdown = this.cells[0][x]; + if (!(dropdown instanceof Element)) { + return super.setCell(x, y, '-'); + } + const activeId = dropdown.getAttribute('aria-activedescendant'); + const activeElem = dropdown.querySelector(`#${activeId}`); + statId = activeElem?.getAttribute('stat-id') as ModuleStatId; + if (!statId) { + return; + } + } + + return super.setCell( + x, + y, + fmtStat(this.modules[y - 1].globalStats[statId], this.modules[0].globalStats[statId]) + ); + } + + addRow(contents: DataGridCell[], pos?: number): number { + const y = super.addRow(contents, pos); + + if (y !== 0) { + for (let x = 1; x < this.width; x++) { + this.fillCell(x, y); + } + } + + return y; + } + + addColumn(): number { + const header = document.createElement('vscode-dropdown'); + for (const id of getModuleStatIds()) { + const opt = document.createElement('vscode-option'); + opt.textContent = getModuleStatName(id); + opt.setAttribute('stat-id', id); + header.appendChild(opt); + } + const pos = super.addColumn([header]); + + // Render after 100ms, the dropdown isn't updated immediately + header.addEventListener('change', (_ev) => setTimeout(this.render.bind(this), 100)); + + // Adds the new column into the table + this.render(); + + // Fill column cells with default values + for (let y = 1; y < this.height; y++) { + this.fillCell(pos, y, Object.values(ModuleStatId)[0]); + } + + return pos; + } + + render() { + this.clearRows(); + + for (const module of this.modules) { + const row = [module.name + (module.isTopLevel ? ' (top-level)' : '')]; + for (const statId of this.moduleStats) { + row.push(module.globalStats[statId].toString()); + } + this.addRow(row); + } + + for (let y = 1; y < this.height; y++) { + for (let x = 1; x < this.width; x++) { + this.fillCell(x, y); + } + } + + super.render(); + } +} + +export class ModuleExplorerGrid extends DataGrid { + private curModule: Module; + + private moduleStats: ModuleStatId[]; + + constructor(initModule: Module) { + super(); + + this.curModule = initModule; + this.moduleStats = []; + + this.addColumn(['Module Name']); + this.addColumn(['Count']); + } + + setModule(module: Module) { + this.curModule = module; + } + + addStat(stat: ModuleStatId) { + if (this.moduleStats.indexOf(stat) !== -1) { + return; + } + this.moduleStats.push(stat); + this.addColumn([getModuleStatName(stat)]); + } + + render() { + this.clearRows(); + + // Current module stats + this.addRow( + ['', ''].concat( + this.moduleStats.map((statId) => + fmtStat(this.curModule.localStats[statId], this.curModule.localStats[statId]) + ) + ) + ); + + // Primitives + const rowFiller: string[] = new Array(this.width - 2).map((_) => ''); + for (const [prim, count] of this.curModule.primitives.entries()) { + this.addRow([`$${prim}`, count.toString()].concat(rowFiller)); + } + + // Child modules + for (const [child, count] of this.curModule.children.entries()) { + const link = document.createElement('vscode-link'); + link.textContent = child.name; + link.addEventListener('click', (_ev) => { + this.dispatchEvent('explorerModuleClicked', {element: this, data: {module: child}}); + }); + + this.addRow( + [link, count.toString()].concat( + this.moduleStats.map((statId) => + fmtStat(child.globalStats[statId] * count, this.curModule.globalStats[statId]) + ) + ) + ); + } + + super.render(); + } +} + +export class ModuleNavigator extends CustomElement { + protected rootElem: HTMLElement; + + private moduleBreadcrumbs: Module[]; + + constructor(initModule: Module) { + super(); + + this.rootElem = document.createElement('p'); + this.moduleBreadcrumbs = [initModule]; + } + + navigateModule(module: Module) { + this.moduleBreadcrumbs.push(module); + } + + render(): void { + this.rootElem.replaceChildren(); + + for (let i = 0; i < this.moduleBreadcrumbs.length; i++) { + const link = document.createElement('vscode-link'); + link.textContent = this.moduleBreadcrumbs[i].name; + link.addEventListener('click', (_ev) => { + this.moduleBreadcrumbs.splice(i + 1); + this.dispatchEvent('explorerFocusUpdate', {element: this, data: {module: this.moduleBreadcrumbs[i]}}); + }); + this.rootElem.append(link); + + if (i != this.moduleBreadcrumbs.length - 1) { + this.rootElem.append(' > '); + } + } + } +} diff --git a/views/digitaljs/src/viewers/stats/events.ts b/views/digitaljs/src/viewers/stats/events.ts new file mode 100644 index 0000000..7a74498 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/events.ts @@ -0,0 +1,28 @@ +import type {CustomElement} from './elements'; +import type {Module, ModuleStatId} from './modules'; + +interface ModuleClickedEvent { + module: Module; +} + +interface ExplorerFocusUpdateEvent { + module: Module; +} + +interface CheckboxClickedEvent { + id: ModuleStatId; + checked: boolean; +} + +interface CustomEvent { + element: CustomElement; + data: E; +} + +export interface CustomEvents { + explorerModuleClicked: CustomEvent; + explorerFocusUpdate: CustomEvent; + checkboxClicked: CustomEvent; +} + +export type CustomEventListener = (ev: CustomEvents[K]) => void; diff --git a/views/digitaljs/src/viewers/stats/modules.ts b/views/digitaljs/src/viewers/stats/modules.ts new file mode 100644 index 0000000..71f80a8 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/modules.ts @@ -0,0 +1,148 @@ +import type {YosysModuleStats} from '../../types'; + +export enum ModuleStatId { + memoryCount = 'memoryCount', + memoryBitCount = 'memoryBitCount', + processCount = 'processCount', + cellCount = 'cellCount' +} + +type ModuleStats = Record; + +export const getModuleStatIds = (): ModuleStatId[] => { + return Object.values(ModuleStatId); +}; + +export const getModuleStatName = (stat: ModuleStatId): string => { + switch (stat) { + case ModuleStatId.memoryCount: + return 'Memories'; + case ModuleStatId.memoryBitCount: + return 'Memory Bits'; + case ModuleStatId.processCount: + return 'Processes'; + case ModuleStatId.cellCount: + return 'Cells'; + default: + return ''; + } +}; + +export class Module { + private _children: Map; + private parents: Set; + private primitiveModules: Map; + + public readonly name: string; + public readonly localStats: ModuleStats; + private _globalStats: ModuleStats | null; + + constructor(name: string, stats: YosysModuleStats) { + this._children = new Map(); + this.parents = new Set(); + this.primitiveModules = new Map(); + + this.name = name; + this.localStats = { + memoryCount: stats.num_memories, + memoryBitCount: stats.num_memory_bits, + processCount: stats.num_processes, + cellCount: stats.num_cells + }; + this._globalStats = null; + } + + get primitives(): Map { + return this.primitiveModules; + } + + get children(): Map { + return this._children; + } + + get isTopLevel(): boolean { + return this.parents.size === 0; + } + + get globalStats(): ModuleStats { + if (this._globalStats) { + return this._globalStats; + } + + this._globalStats = structuredClone(this.localStats); + for (const [module, count] of this._children) { + const moduleStats = module.globalStats; + this._globalStats.memoryCount += count * moduleStats.memoryCount; + this._globalStats.memoryBitCount += count * moduleStats.memoryBitCount; + this._globalStats.cellCount += count * moduleStats.cellCount; + } + + return this._globalStats; + } + + addPrimitive(prim: string, count: number) { + const curCount = this.primitiveModules.get(prim) || 0; + this.primitiveModules.set(prim, curCount + count); + } + + addChild(module: Module, count: number) { + const curCount = this._children.get(module) || 0; + this._children.set(module, curCount + count); + module.addParent(this); + } + + protected addParent(module: Module) { + this.parents.add(module); + } +} + +const resolveModuleDeps = (name: string, stats: YosysModuleStats, resolved: Map): Module | null => { + const module = new Module(name, stats); + for (const [depName, count] of Object.entries(stats.num_cells_by_type)) { + if (depName.startsWith('$')) { + module.addPrimitive(depName.slice(1), count); + continue; + } + + const childDep = resolved.get(depName); + if (!childDep) { + // Cannot fully be resolved yet + return null; + } + module.addChild(childDep, count); + } + + return module; +}; + +export const buildModuleTree = (modules: Record): Module[] => { + const modEntries = Object.entries(modules); + const modCount = modEntries.length; + if (!modCount) { + return []; + } + + const resolved = new Map(); + while (resolved.size < modCount) { + const oldSize = resolved.size; + for (let i = modEntries.length - 1; i >= 0; i--) { + const [modName, stats] = modEntries[i]; + const sanitizedName = modName.slice(1); // Strip leading "\" + if (sanitizedName in resolved.keys()) { + continue; + } + + const module = resolveModuleDeps(sanitizedName, stats, resolved); + if (!module) { + continue; + } + resolved.set(sanitizedName, module); + modEntries.splice(i, 1); + } + if (resolved.size <= oldSize) { + throw new Error('Cyclic dependency loop detected; cannot resolve module dependency tree!'); + } + } + + return Array.from(resolved.values()).reverse(); +}; diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts new file mode 100644 index 0000000..abfdfc5 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -0,0 +1,96 @@ +import type {View} from '../../main'; +import type {ForeignViewMessage} from '../../messages'; +import type {YosysStats} from '../../types'; +import {BaseViewer} from '../base'; + +import {ModuleExplorerGrid, ModuleNavigator, ModuleOverviewGrid} from './elements'; +import {type Module, buildModuleTree} from './modules'; + +export class StatsViewer extends BaseViewer { + handleForeignViewMessage(message: ForeignViewMessage): void { + console.log('Foreign message:'); + console.log(message); + } + + private modules: Module[]; + + private overviewGrid: ModuleOverviewGrid; + + private explorerNavigator: ModuleNavigator; + private explorerGrid: ModuleExplorerGrid; + + constructor(mainView: View, initData: YosysStats) { + super(mainView, initData); + + this.modules = buildModuleTree(this.data.modules); + if (!this.modules) { + throw new Error('No circuit modules found to display!'); + } + + this.overviewGrid = new ModuleOverviewGrid(this.modules); + + this.explorerNavigator = new ModuleNavigator(this.modules[0]); + this.explorerNavigator.addEventListener('explorerFocusUpdate', (event) => { + this.renderExplorer(event.data.module); + }); + + this.explorerGrid = new ModuleExplorerGrid(this.modules[0]); + this.explorerGrid.addEventListener('explorerModuleClicked', (event) => { + this.explorerNavigator.navigateModule(event.data.module); + this.renderExplorer(event.data.module); + }); + } + + private renderExplorer(module: Module) { + this.explorerGrid.setModule(module); + this.explorerGrid.render(); + + this.explorerNavigator.render(); + } + + async render(): Promise { + this.root.replaceChildren(); + + // ** Overview Table ** + const overviewHeader = document.createElement('h2'); + overviewHeader.textContent = 'Circuit overview'; + this.root.appendChild(overviewHeader); + + const btn = document.createElement('button'); + btn.textContent = 'Click for more columns'; + btn.addEventListener('click', (_ev) => { + this.overviewGrid.addColumn(); + }); + this.root.appendChild(btn); + + this.root.appendChild(document.createElement('br')); + + this.overviewGrid.render(); + this.root.appendChild(this.overviewGrid.element); + + // ** Divider ** + this.root.appendChild(document.createElement('br')); + this.root.appendChild(document.createElement('vscode-divider')); + + // ** Circuit explorer ** + const explorerHeader = document.createElement('h2'); + explorerHeader.textContent = 'Circuit explorer'; + this.root.appendChild(explorerHeader); + + this.explorerNavigator.render(); + this.root.appendChild(this.explorerNavigator.element); + this.explorerGrid.render(); + this.root.appendChild(this.explorerGrid.element); + + // ** Other stuff ** + const button = document.createElement('button'); + button.textContent = 'Click for broadcast'; + button.addEventListener('click', (_ev) => { + this.broadcastMessage({ + type: 'moduleFocus', + module: 'test_module_goes_here' + }); + }); + this.root.appendChild(button); + } +} From df27ee73a4efdbdc9b1e78b3bd88345f523a1fbf Mon Sep 17 00:00:00 2001 From: Mike A Date: Wed, 30 Aug 2023 16:44:54 +0200 Subject: [PATCH 06/31] More column control buttons --- views/digitaljs/src/viewers/stats/elements.ts | 60 ++++++++++++++----- views/digitaljs/src/viewers/stats/stats.ts | 15 +++-- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 46ad269..562f98c 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -67,6 +67,12 @@ export class DataGrid extends CustomElement { return pos; } + delColumn(pos: number) { + for (const row of this.cells) { + row.splice(pos, 1); + } + } + addRow(contents: DataGridCell[], pos?: number): number { contents = contents.slice(0, this.width); if (contents.length < this.width) { @@ -135,18 +141,14 @@ export class DataGrid extends CustomElement { } export class ModuleOverviewGrid extends DataGrid { - private moduleStats: ModuleStatId[]; - private readonly modules: Module[]; constructor(modules: Module[]) { super(); this.modules = modules; - this.moduleStats = []; - super.addColumn(['Module Name']); - super.addColumn(['Count']); + this.reset(); } private fillCell(x: number, y: number, statId?: ModuleStatId) { @@ -161,12 +163,13 @@ export class ModuleOverviewGrid extends DataGrid { if (!statId) { // Find active column stat id - const dropdown = this.cells[0][x]; - if (!(dropdown instanceof Element)) { + const header = this.cells[0][x]; + if (!(header instanceof Element)) { return super.setCell(x, y, '-'); } - const activeId = dropdown.getAttribute('aria-activedescendant'); - const activeElem = dropdown.querySelector(`#${activeId}`); + const dropdown = header.querySelector('vscode-dropdown'); + const activeId = dropdown?.getAttribute('aria-activedescendant'); + const activeElem = dropdown?.querySelector(`#${activeId}`); statId = activeElem?.getAttribute('stat-id') as ModuleStatId; if (!statId) { return; @@ -193,13 +196,28 @@ export class ModuleOverviewGrid extends DataGrid { } addColumn(): number { - const header = document.createElement('vscode-dropdown'); + const header = document.createElement('div'); + + const delBtn = document.createElement('vscode-button'); + delBtn.innerHTML = /* html */ ``; + delBtn.addEventListener('click', (_ev) => { + const coli = this.cells[0].indexOf(header); + if (coli !== -1) { + this.delColumn(coli); + this.render(); + } + }); + header.appendChild(delBtn); + + const dropdown = document.createElement('vscode-dropdown'); for (const id of getModuleStatIds()) { const opt = document.createElement('vscode-option'); opt.textContent = getModuleStatName(id); opt.setAttribute('stat-id', id); - header.appendChild(opt); + dropdown.appendChild(opt); } + header.appendChild(dropdown); + const pos = super.addColumn([header]); // Render after 100ms, the dropdown isn't updated immediately @@ -216,15 +234,25 @@ export class ModuleOverviewGrid extends DataGrid { return pos; } + reset() { + this.clearRows(); + while (this.width > 0) { + this.delColumn(0); + } + + super.addColumn(['Module Name']); + super.addColumn(['Count']); + this.addColumn(); + this.addColumn(); + + setTimeout(this.render.bind(this), 100); + } + render() { this.clearRows(); for (const module of this.modules) { - const row = [module.name + (module.isTopLevel ? ' (top-level)' : '')]; - for (const statId of this.moduleStats) { - row.push(module.globalStats[statId].toString()); - } - this.addRow(row); + this.addRow([module.name + (module.isTopLevel ? ' (top-level)' : '')]); } for (let y = 1; y < this.height; y++) { diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index abfdfc5..f88cb24 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -56,12 +56,19 @@ export class StatsViewer extends BaseViewer { overviewHeader.textContent = 'Circuit overview'; this.root.appendChild(overviewHeader); - const btn = document.createElement('button'); - btn.textContent = 'Click for more columns'; - btn.addEventListener('click', (_ev) => { + const addColBtn = document.createElement('vscode-button'); + addColBtn.innerHTML = /* html */ ``; + addColBtn.addEventListener('click', (_ev) => { this.overviewGrid.addColumn(); }); - this.root.appendChild(btn); + this.root.appendChild(addColBtn); + + const resetBtn = document.createElement('vscode-button'); + resetBtn.innerHTML = /* html */ ``; + resetBtn.addEventListener('click', (_ev) => { + this.overviewGrid.reset(); + }); + this.root.appendChild(resetBtn); this.root.appendChild(document.createElement('br')); From f9c23702fc0510f6f0c4a27ce06a502bb1e8f4f6 Mon Sep 17 00:00:00 2001 From: Mike A Date: Wed, 30 Aug 2023 17:27:42 +0200 Subject: [PATCH 07/31] Avoid unnecessary re-renders + fix element events --- views/digitaljs/src/main.ts | 10 +++++----- views/digitaljs/src/viewers/base.ts | 2 +- views/digitaljs/src/viewers/diagram/diagram.ts | 2 +- views/digitaljs/src/viewers/stats/elements.ts | 10 ++++++++-- views/digitaljs/src/viewers/stats/stats.ts | 6 +++++- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/views/digitaljs/src/main.ts b/views/digitaljs/src/main.ts index 9062f63..a283c1f 100644 --- a/views/digitaljs/src/main.ts +++ b/views/digitaljs/src/main.ts @@ -31,7 +31,7 @@ export class View { addEventListener('resize', this.handleResize.bind(this)); if (this.state.document) { - this.renderDocument(); + this.renderDocument(false); } else { this.sendMessage({ type: 'ready' @@ -53,7 +53,7 @@ export class View { this.updateState({ document: message.data.document }); - this.renderDocument(); + this.renderDocument(false); break; } case 'broadcast': { @@ -69,7 +69,7 @@ export class View { } private handleResize() { - this.renderDocument(); + this.renderDocument(true); } private findViewer(): BaseViewer { @@ -91,7 +91,7 @@ export class View { } } - private renderDocument() { + private renderDocument(isUpdate: boolean) { try { if (!this.state.document) { throw new Error('No data to render document!'); @@ -99,7 +99,7 @@ export class View { this.viewer = this.findViewer(); } - this.viewer.render().catch((err) => this.handleError(err, this.viewer)); + this.viewer.render(isUpdate).catch((err) => this.handleError(err, this.viewer)); } catch (err) { this.handleError(err); } diff --git a/views/digitaljs/src/viewers/base.ts b/views/digitaljs/src/viewers/base.ts index db2ef3a..946981e 100644 --- a/views/digitaljs/src/viewers/base.ts +++ b/views/digitaljs/src/viewers/base.ts @@ -10,7 +10,7 @@ export abstract class BaseViewer { this.data = initData; } - abstract render(): Promise; + abstract render(isUpdate: boolean): Promise; abstract handleForeignViewMessage(message: ForeignViewMessage): void; diff --git a/views/digitaljs/src/viewers/diagram/diagram.ts b/views/digitaljs/src/viewers/diagram/diagram.ts index 9950b7f..bdff2e0 100644 --- a/views/digitaljs/src/viewers/diagram/diagram.ts +++ b/views/digitaljs/src/viewers/diagram/diagram.ts @@ -31,7 +31,7 @@ export class DiagramViewer extends BaseViewer { console.log(message); } - async render() { + async render(_isUpdate: boolean) { // Convert from Yosys netlist to DigitalJS format const digitalJs = yosys2digitaljs(this.data); diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 562f98c..0f51412 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -13,12 +13,18 @@ const fmtStat = (stat1: number, stat2: number): string => { export abstract class CustomElement { protected abstract rootElem: HTMLElement; + private eventsElem: Element; + + constructor() { + this.eventsElem = document.createElement('events'); + } + addEventListener(type: K, listener: CustomEventListener) { - this.element.addEventListener(type, (ev: CustomEventInit) => listener(ev.detail)); + this.eventsElem.addEventListener(type, (ev: CustomEventInit) => listener(ev.detail)); } protected dispatchEvent(type: K, data: CustomEvents[K]) { - this.element.dispatchEvent(new CustomEvent(type, {detail: data})); + this.eventsElem.dispatchEvent(new CustomEvent(type, {detail: data})); } get element() { diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index f88cb24..9d18e0f 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -48,7 +48,11 @@ export class StatsViewer extends BaseViewer { this.explorerNavigator.render(); } - async render(): Promise { + async render(isUpdate: boolean): Promise { + if (isUpdate) { + // All elements are dynamically resized so we don't need to redraw + return; + } this.root.replaceChildren(); // ** Overview Table ** From 46c6390aad75d85a2dfb4d69bdb847eedfa3fada Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 16 Sep 2023 21:22:44 +0200 Subject: [PATCH 08/31] Update module focus message to contain breadcrumbs --- src/extension/editors/messages.ts | 2 +- views/digitaljs/src/messages.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension/editors/messages.ts b/src/extension/editors/messages.ts index 3446f9b..1eead94 100644 --- a/src/extension/editors/messages.ts +++ b/src/extension/editors/messages.ts @@ -1,6 +1,6 @@ interface ForeignViewMessageModuleFocus { type: 'moduleFocus'; - module: string; + breadcrumbs: string[]; } export type ForeignViewMessage = ForeignViewMessageModuleFocus; diff --git a/views/digitaljs/src/messages.ts b/views/digitaljs/src/messages.ts index 3446f9b..1eead94 100644 --- a/views/digitaljs/src/messages.ts +++ b/views/digitaljs/src/messages.ts @@ -1,6 +1,6 @@ interface ForeignViewMessageModuleFocus { type: 'moduleFocus'; - module: string; + breadcrumbs: string[]; } export type ForeignViewMessage = ForeignViewMessageModuleFocus; From 0e1742ddbf84b9e50ea106333858d3c80d2b8a82 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 23 Sep 2023 12:44:01 +0200 Subject: [PATCH 09/31] Implement storing of permanent data from views --- src/extension/editors/base.ts | 41 ++++++++++-- src/extension/editors/messages.ts | 25 +++++++ src/extension/editors/nextpnr.ts | 14 +++- src/extension/editors/project.ts | 18 ++++- src/extension/editors/yosys.ts | 17 ++++- views/digitaljs/src/globalStore.ts | 78 ++++++++++++++++++++++ views/digitaljs/src/main.ts | 20 +++++- views/digitaljs/src/messages.ts | 23 +++++++ views/digitaljs/src/viewers/base.ts | 8 +++ views/digitaljs/src/viewers/stats/stats.ts | 2 +- 10 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 views/digitaljs/src/globalStore.ts diff --git a/src/extension/editors/base.ts b/src/extension/editors/base.ts index a31879a..3976ed3 100644 --- a/src/extension/editors/base.ts +++ b/src/extension/editors/base.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import type {Projects} from '../projects/index.js'; import {getWebviewUri} from '../util.js'; -import {type ViewMessage} from './messages.js'; +import type {GlobalStoreMessage, ViewMessage} from './messages.js'; export abstract class BaseEditor implements vscode.CustomTextEditorProvider { protected readonly context: vscode.ExtensionContext; @@ -121,16 +121,43 @@ export abstract class BaseEditor implements vscode.CustomTextEditorProvider { .join('\n'); } + protected onDidReceiveMessage( + _document: vscode.TextDocument, + webview: vscode.Webview, + message: ViewMessage | GlobalStoreMessage + ): boolean { + if (message.type === 'globalStore') { + if (message.action === 'set') { + this.context.globalState.update(message.name, message.value).then(() => { + const response: GlobalStoreMessage = { + type: 'globalStore', + action: 'result', + transaction: message.transaction + }; + webview.postMessage(response); + }); + return true; + } else if (message.action === 'get') { + const value = this.context.globalState.get(message.name) || ({} as object); + const response: GlobalStoreMessage = { + type: 'globalStore', + action: 'result', + transaction: message.transaction, + result: value + }; + webview.postMessage(response); + + return true; + } + } + + return false; + } + protected abstract getStylePaths(): string[][]; protected abstract getScriptPaths(): string[][]; - protected abstract onDidReceiveMessage( - document: vscode.TextDocument, - webview: vscode.Webview, - message: ViewMessage - ): void; - protected abstract onSave(document: vscode.TextDocument, webview: vscode.Webview): void; protected abstract onClose(document: vscode.TextDocument, webview: vscode.Webview): void; diff --git a/src/extension/editors/messages.ts b/src/extension/editors/messages.ts index 1eead94..dae6d3e 100644 --- a/src/extension/editors/messages.ts +++ b/src/extension/editors/messages.ts @@ -36,3 +36,28 @@ interface ViewMessageRequestSave { } export type ViewMessage = ViewMessageReady | ViewMessageChange | MessageBroadcast | ViewMessageRequestSave; + +interface GlobalStore { + type: 'globalStore'; +} + +interface GlobalStoreSet extends GlobalStore { + action: 'set'; + transaction: string; + name: string; + value: object; +} + +interface GlobalStoreGet extends GlobalStore { + action: 'get'; + transaction: string; + name: string; +} + +interface GlobalStoreResult extends GlobalStore { + action: 'result'; + transaction: string; + result?: object; +} + +export type GlobalStoreMessage = GlobalStoreSet | GlobalStoreGet | GlobalStoreResult; diff --git a/src/extension/editors/nextpnr.ts b/src/extension/editors/nextpnr.ts index 2136a19..c0ad518 100644 --- a/src/extension/editors/nextpnr.ts +++ b/src/extension/editors/nextpnr.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import {getWebviewUri} from '../util.js'; import {BaseEditor} from './base.js'; -import {type ViewMessage} from './messages.js'; +import type {GlobalStoreMessage, ViewMessage} from './messages.js'; export class NextpnrEditor extends BaseEditor { public static getViewType() { @@ -35,13 +35,23 @@ export class NextpnrEditor extends BaseEditor { `; } - protected onDidReceiveMessage(document: vscode.TextDocument, webview: vscode.Webview, message: ViewMessage): void { + protected onDidReceiveMessage( + document: vscode.TextDocument, + webview: vscode.Webview, + message: ViewMessage | GlobalStoreMessage + ): boolean { + if (super.onDidReceiveMessage(document, webview, message)) { + return true; + } if (message.type === 'ready') { webview.postMessage({ type: 'document', document: document.getText() }); + return true; } + + return false; } protected onSave(_document: vscode.TextDocument, _webview: vscode.Webview): void { diff --git a/src/extension/editors/project.ts b/src/extension/editors/project.ts index 846efd8..096a192 100644 --- a/src/extension/editors/project.ts +++ b/src/extension/editors/project.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import {Project} from '../projects/index.js'; import {BaseEditor} from './base.js'; -import {type ViewMessage} from './messages.js'; +import type {GlobalStoreMessage, ViewMessage} from './messages.js'; export class ProjectEditor extends BaseEditor { public static getViewType() { @@ -34,7 +34,14 @@ export class ProjectEditor extends BaseEditor { } } - protected onDidReceiveMessage(document: vscode.TextDocument, webview: vscode.Webview, message: ViewMessage): void { + protected onDidReceiveMessage( + document: vscode.TextDocument, + webview: vscode.Webview, + message: ViewMessage | GlobalStoreMessage + ): boolean { + if (super.onDidReceiveMessage(document, webview, message)) { + return true; + } console.log(message); if (message.type === 'ready') { @@ -48,15 +55,20 @@ export class ProjectEditor extends BaseEditor { project: Project.serialize(project) }); } + return true; } else if (message.type === 'change') { if (document.getText() === message.document) { - return; + return true; } const edit = new vscode.WorkspaceEdit(); edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), message.document as string); vscode.workspace.applyEdit(edit); + + return true; } + + return false; } protected async onSave(document: vscode.TextDocument, webview: vscode.Webview) { diff --git a/src/extension/editors/yosys.ts b/src/extension/editors/yosys.ts index d005622..53da35f 100644 --- a/src/extension/editors/yosys.ts +++ b/src/extension/editors/yosys.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import * as util from '../util.js'; import {BaseEditor} from './base.js'; -import {type ViewMessage} from './messages.js'; +import type {GlobalStoreMessage, ViewMessage} from './messages.js'; export class YosysEditor extends BaseEditor { private static activeViews: Set = new Set(); @@ -43,12 +43,21 @@ export class YosysEditor extends BaseEditor { `; } - protected onDidReceiveMessage(document: vscode.TextDocument, webview: vscode.Webview, message: ViewMessage): void { + protected onDidReceiveMessage( + document: vscode.TextDocument, + webview: vscode.Webview, + message: ViewMessage | GlobalStoreMessage + ): boolean { + if (super.onDidReceiveMessage(document, webview, message)) { + return true; + } + if (message.type === 'ready') { webview.postMessage({ type: 'document', document: document.getText() }); + return true; } else if (message.type === 'broadcast') { for (const view of YosysEditor.activeViews) { if (view === webview) { @@ -56,6 +65,7 @@ export class YosysEditor extends BaseEditor { } view.postMessage(message); } + return true; } else if (message.type === 'requestSave') { // Save to project root, or the parent dir of the current editor's file if we can't find it const projectRoot = util.findProjectRoot(document.uri) || util.getParentUri(document.uri); @@ -70,7 +80,10 @@ export class YosysEditor extends BaseEditor { } this.showSaveNotification(path); }); + return true; } + + return false; } protected onSave(_document: vscode.TextDocument, _webview: vscode.Webview): void { diff --git a/views/digitaljs/src/globalStore.ts b/views/digitaljs/src/globalStore.ts new file mode 100644 index 0000000..df2cd49 --- /dev/null +++ b/views/digitaljs/src/globalStore.ts @@ -0,0 +1,78 @@ +import type {GlobalStoreMessage} from './messages'; +import {vscode} from './vscode'; + +interface TransactionSet { + type: 'set'; + resolve: (value: void) => unknown; +} + +interface TransactionGet { + type: 'get'; + resolve: (value: object) => unknown; +} + +type Transaction = TransactionSet | TransactionGet; + +export class GlobalStoreConnector { + private transactions: Record; + + constructor() { + this.transactions = {}; + } + + private sendMessage(message: GlobalStoreMessage) { + vscode.postMessage(message); + } + + public onMessage(message: GlobalStoreMessage) { + if (message.action !== 'result') { + return; + } + + const transaction = this.transactions[message.transaction]; + if (transaction === undefined) { + // Not a critical error, so do not throw + console.error(`GlobalState transaction ${transaction} not found!`); + return; + } + + if (transaction.type === 'set') { + transaction.resolve(); + } else if (transaction.type === 'get') { + transaction.resolve(message.result || {}); + } + } + + public set(name: string, value: object): Promise { + const transUuid = Math.floor(Math.random() * 100_000); + const promise = new Promise((resolve: (value: void) => unknown, _reject) => { + this.transactions[transUuid] = {type: 'set', resolve}; + }); + + this.sendMessage({ + type: 'globalStore', + transaction: transUuid, + action: 'set', + name: name, + value: value + }); + + return promise; + } + + public get(name: string): Promise { + const transUuid = Math.floor(Math.random() * 100_000); + const promise = new Promise((resolve: (value: object) => unknown, _reject) => { + this.transactions[transUuid] = {type: 'get', resolve}; + }); + + this.sendMessage({ + type: 'globalStore', + transaction: transUuid, + action: 'get', + name: name + }); + + return promise; + } +} diff --git a/views/digitaljs/src/main.ts b/views/digitaljs/src/main.ts index a283c1f..ace0faa 100644 --- a/views/digitaljs/src/main.ts +++ b/views/digitaljs/src/main.ts @@ -2,8 +2,9 @@ import '@vscode/codicons/dist/codicon.css'; import {allComponents} from '@vscode/webview-ui-toolkit/dist/toolkit.js'; import 'jquery-ui/dist/jquery-ui.min.js'; +import {GlobalStoreConnector} from './globalStore'; import './main.css'; -import type {EditorMessage, ForeignViewMessage, ViewMessage} from './messages'; +import type {EditorMessage, ForeignViewMessage, GlobalStoreMessage, ViewMessage} from './messages'; import type {YosysFile} from './types'; import {type BaseViewer, DiagramViewer, StatsViewer} from './viewers'; import {vscode} from './vscode'; @@ -20,12 +21,15 @@ export class View { private state: State; private viewer: BaseViewer | null; + private readonly globalStore: GlobalStoreConnector; constructor(root: HTMLDivElement, state: State) { this.root = root; this.state = state; this.viewer = null; + this.globalStore = new GlobalStoreConnector(); + addEventListener('message', this.handleMessage.bind(this)); addEventListener('messageerror', this.handleMessageError.bind(this)); addEventListener('resize', this.handleResize.bind(this)); @@ -47,7 +51,7 @@ export class View { vscode.setState(this.state); } - private handleMessage(message: MessageEvent) { + private handleMessage(message: MessageEvent) { switch (message.data.type) { case 'document': { this.updateState({ @@ -60,6 +64,10 @@ export class View { if (this.viewer) { this.viewer.handleForeignViewMessage(message.data.message); } + break; + } + case 'globalStore': { + this.globalStore.onMessage(message.data); } } } @@ -105,6 +113,14 @@ export class View { } } + storeValue(name: string, value: object): Promise { + return this.globalStore.set(name, value); + } + + getValue(name: string): Promise { + return this.globalStore.get(name); + } + sendMessage(message: ViewMessage) { vscode.postMessage(message); } diff --git a/views/digitaljs/src/messages.ts b/views/digitaljs/src/messages.ts index 1eead94..b08fde2 100644 --- a/views/digitaljs/src/messages.ts +++ b/views/digitaljs/src/messages.ts @@ -36,3 +36,26 @@ interface ViewMessageRequestSave { } export type ViewMessage = ViewMessageReady | ViewMessageChange | MessageBroadcast | ViewMessageRequestSave; + +interface GlobalStore { + type: 'globalStore'; + transaction: number; +} + +interface GlobalStoreSet extends GlobalStore { + action: 'set'; + name: string; + value: object; +} + +interface GlobalStoreGet extends GlobalStore { + action: 'get'; + name: string; +} + +interface GlobalStoreResult extends GlobalStore { + action: 'result'; + result?: object; +} + +export type GlobalStoreMessage = GlobalStoreSet | GlobalStoreGet | GlobalStoreResult; diff --git a/views/digitaljs/src/viewers/base.ts b/views/digitaljs/src/viewers/base.ts index 946981e..b9eb37c 100644 --- a/views/digitaljs/src/viewers/base.ts +++ b/views/digitaljs/src/viewers/base.ts @@ -18,6 +18,14 @@ export abstract class BaseViewer { return this.mainView.root; } + protected storeValue(name: string, value: object): Promise { + return this.mainView.storeValue(name, value); + } + + protected getValue(name: string): Promise { + return this.mainView.getValue(name); + } + protected broadcastMessage(message: ForeignViewMessage) { return this.mainView.broadcastMessage(message); } diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index 9d18e0f..68a953b 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -99,7 +99,7 @@ export class StatsViewer extends BaseViewer { button.addEventListener('click', (_ev) => { this.broadcastMessage({ type: 'moduleFocus', - module: 'test_module_goes_here' + breadcrumbs: ['test_module_goes_here'] }); }); this.root.appendChild(button); From 1cb2a13aacf137e7db05498bf19f66d535717f5b Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 23 Sep 2023 14:41:29 +0200 Subject: [PATCH 10/31] Focus statistics from digitaljs view --- views/digitaljs/package-lock.json | 4 +- .../digitaljs/src/viewers/diagram/diagram.ts | 13 ++++++- views/digitaljs/src/viewers/stats/elements.ts | 10 +++-- views/digitaljs/src/viewers/stats/stats.ts | 38 ++++++++++++------- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/views/digitaljs/package-lock.json b/views/digitaljs/package-lock.json index d38d968..485bd05 100644 --- a/views/digitaljs/package-lock.json +++ b/views/digitaljs/package-lock.json @@ -278,8 +278,8 @@ } }, "node_modules/digitaljs": { - "version": "0.14.0", - "resolved": "git+ssh://git@github.com/EDAcation/digitaljs.git#65605962911923d368c8e266705b7022fa0b2114", + "version": "0.14.1", + "resolved": "git+ssh://git@github.com/EDAcation/digitaljs.git#3fd56fa4c2c0f08c52aca8154f8368b5bbc411e1", "license": "BSD-2-Clause", "dependencies": { "3vl": "^1.0.1", diff --git a/views/digitaljs/src/viewers/diagram/diagram.ts b/views/digitaljs/src/viewers/diagram/diagram.ts index ef8fafe..86deb62 100644 --- a/views/digitaljs/src/viewers/diagram/diagram.ts +++ b/views/digitaljs/src/viewers/diagram/diagram.ts @@ -7,10 +7,13 @@ import type {YosysRTL} from '../../types'; import {BaseViewer} from '../base'; // TODO: better typing - should be fixed from our digitaljs fork +type Model = any; // eslint-disable-line @typescript-eslint/no-explicit-any + interface ButtonCallback { circuit: Circuit; - model: any; // eslint-disable-line @typescript-eslint/no-explicit-any + model: Model; paper: any; // eslint-disable-line @typescript-eslint/no-explicit-any + navHistory: Model[]; } const getSvg = (svgElem: Element, width: number, height: number): string => { @@ -40,6 +43,14 @@ export class DiagramViewer extends BaseViewer { callback: ({model, paper}: ButtonCallback) => { this.requestExport(paper.svg, `${model.get('label')}.svg`); } + }, + { + id: 'showStats', + buttonText: '🔍', + callback: ({navHistory}: ButtonCallback) => { + const cellHistory = navHistory.map((model) => model.get('celltype') as string); + this.broadcastMessage({type: 'moduleFocus', breadcrumbs: cellHistory}); + } } ]; diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 0f51412..d893171 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -353,16 +353,18 @@ export class ModuleNavigator extends CustomElement { this.moduleBreadcrumbs.push(module); } + navigateSplice(i: number) { + this.moduleBreadcrumbs.splice(i + 1); + this.dispatchEvent('explorerFocusUpdate', {element: this, data: {module: this.moduleBreadcrumbs[i]}}); + } + render(): void { this.rootElem.replaceChildren(); for (let i = 0; i < this.moduleBreadcrumbs.length; i++) { const link = document.createElement('vscode-link'); link.textContent = this.moduleBreadcrumbs[i].name; - link.addEventListener('click', (_ev) => { - this.moduleBreadcrumbs.splice(i + 1); - this.dispatchEvent('explorerFocusUpdate', {element: this, data: {module: this.moduleBreadcrumbs[i]}}); - }); + link.addEventListener('click', (_ev) => this.navigateSplice(i)); this.rootElem.append(link); if (i != this.moduleBreadcrumbs.length - 1) { diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index 68a953b..77ebe64 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -8,8 +8,22 @@ import {type Module, buildModuleTree} from './modules'; export class StatsViewer extends BaseViewer { handleForeignViewMessage(message: ForeignViewMessage): void { - console.log('Foreign message:'); - console.log(message); + if (message.type === 'moduleFocus') { + // Focus explorer navigator according to breadcrumbs from djs viewer + this.explorerNavigator.navigateSplice(0); + let lastModule: Module = this.modules[0]; + for (const moduleName of message.breadcrumbs) { + const module = this.getModule(moduleName); + if (!module) { + throw new Error(`Unknown module: ${moduleName}`); + } + this.explorerNavigator.navigateModule(module); + + lastModule = module; + } + + this.renderExplorer(lastModule); + } } private modules: Module[]; @@ -41,6 +55,15 @@ export class StatsViewer extends BaseViewer { }); } + private getModule(name: string): Module | null { + for (const module of this.modules) { + if (module.name === name) { + return module; + } + } + return null; + } + private renderExplorer(module: Module) { this.explorerGrid.setModule(module); this.explorerGrid.render(); @@ -92,16 +115,5 @@ export class StatsViewer extends BaseViewer { this.root.appendChild(this.explorerNavigator.element); this.explorerGrid.render(); this.root.appendChild(this.explorerGrid.element); - - // ** Other stuff ** - const button = document.createElement('button'); - button.textContent = 'Click for broadcast'; - button.addEventListener('click', (_ev) => { - this.broadcastMessage({ - type: 'moduleFocus', - breadcrumbs: ['test_module_goes_here'] - }); - }); - this.root.appendChild(button); } } From 739047eaa09d1f1fe016665faf12ad484c298b43 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 23 Sep 2023 19:54:27 +0200 Subject: [PATCH 11/31] Save module overview table settings --- views/digitaljs/src/viewers/stats/elements.ts | 29 +++++++++++++--- views/digitaljs/src/viewers/stats/events.ts | 6 ++++ views/digitaljs/src/viewers/stats/stats.ts | 33 +++++++++++++++++-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index d893171..0c61eee 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -77,6 +77,10 @@ export class DataGrid extends CustomElement { for (const row of this.cells) { row.splice(pos, 1); } + + if (pos >= 2) { + this.dispatchEvent('overviewGridStatUpdate', {element: this, data: {index: pos - 2, statId: null}}); + } } addRow(contents: DataGridCell[], pos?: number): number { @@ -182,11 +186,18 @@ export class ModuleOverviewGrid extends DataGrid { } } - return super.setCell( + const res = super.setCell( x, y, fmtStat(this.modules[y - 1].globalStats[statId], this.modules[0].globalStats[statId]) ); + + this.dispatchEvent('overviewGridStatUpdate', { + element: this, + data: {index: x - 2, statId} + }); + + return res; } addRow(contents: DataGridCell[], pos?: number): number { @@ -201,7 +212,7 @@ export class ModuleOverviewGrid extends DataGrid { return y; } - addColumn(): number { + addStat(statId?: ModuleStatId): number { const header = document.createElement('div'); const delBtn = document.createElement('vscode-button'); @@ -216,12 +227,22 @@ export class ModuleOverviewGrid extends DataGrid { header.appendChild(delBtn); const dropdown = document.createElement('vscode-dropdown'); - for (const id of getModuleStatIds()) { + const addOption = (id: ModuleStatId) => { const opt = document.createElement('vscode-option'); opt.textContent = getModuleStatName(id); opt.setAttribute('stat-id', id); dropdown.appendChild(opt); + }; + + if (statId) { + addOption(statId); } + for (const id of getModuleStatIds()) { + if (id !== statId) { + addOption(id); + } + } + header.appendChild(dropdown); const pos = super.addColumn([header]); @@ -248,8 +269,6 @@ export class ModuleOverviewGrid extends DataGrid { super.addColumn(['Module Name']); super.addColumn(['Count']); - this.addColumn(); - this.addColumn(); setTimeout(this.render.bind(this), 100); } diff --git a/views/digitaljs/src/viewers/stats/events.ts b/views/digitaljs/src/viewers/stats/events.ts index 7a74498..6bff4a1 100644 --- a/views/digitaljs/src/viewers/stats/events.ts +++ b/views/digitaljs/src/viewers/stats/events.ts @@ -1,6 +1,11 @@ import type {CustomElement} from './elements'; import type {Module, ModuleStatId} from './modules'; +interface OverviewStatUpdateEvent { + index: number; + statId: ModuleStatId | null; +} + interface ModuleClickedEvent { module: Module; } @@ -20,6 +25,7 @@ interface CustomEvent { } export interface CustomEvents { + overviewGridStatUpdate: CustomEvent; explorerModuleClicked: CustomEvent; explorerFocusUpdate: CustomEvent; checkboxClicked: CustomEvent; diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index 77ebe64..e438929 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -4,7 +4,11 @@ import type {YosysStats} from '../../types'; import {BaseViewer} from '../base'; import {ModuleExplorerGrid, ModuleNavigator, ModuleOverviewGrid} from './elements'; -import {type Module, buildModuleTree} from './modules'; +import {type Module, type ModuleStatId, buildModuleTree} from './modules'; + +interface OverviewGridSettings { + columns: ModuleStatId[]; +} export class StatsViewer extends BaseViewer { handleForeignViewMessage(message: ForeignViewMessage): void { @@ -28,6 +32,7 @@ export class StatsViewer extends BaseViewer { private modules: Module[]; + private overviewSettings: OverviewGridSettings; private overviewGrid: ModuleOverviewGrid; private explorerNavigator: ModuleNavigator; @@ -42,6 +47,30 @@ export class StatsViewer extends BaseViewer { } this.overviewGrid = new ModuleOverviewGrid(this.modules); + this.overviewGrid.addEventListener('overviewGridStatUpdate', (event) => { + if (this.overviewSettings.columns[event.data.index] === event.data.statId) { + return; + } + if (event.data.statId === null) { + this.overviewSettings.columns.splice(event.data.index, 1); + } else { + this.overviewSettings.columns[event.data.index] = event.data.statId; + } + this.storeValue('djs-stats-overview-settings', this.overviewSettings); + }); + + this.overviewSettings = {columns: []}; + this.getValue('djs-stats-overview-settings').then((value) => { + if (Object.keys(value).length === 0) { + this.overviewSettings = {columns: []}; + } else { + this.overviewSettings = value as OverviewGridSettings; + } + for (const col of this.overviewSettings.columns) { + console.log(`Add ${col}`); + this.overviewGrid.addStat(col); + } + }); this.explorerNavigator = new ModuleNavigator(this.modules[0]); this.explorerNavigator.addEventListener('explorerFocusUpdate', (event) => { @@ -86,7 +115,7 @@ export class StatsViewer extends BaseViewer { const addColBtn = document.createElement('vscode-button'); addColBtn.innerHTML = /* html */ ``; addColBtn.addEventListener('click', (_ev) => { - this.overviewGrid.addColumn(); + this.overviewGrid.addStat(); }); this.root.appendChild(addColBtn); From eaffbe19d01649b4974eaac64670fadea64cf32e Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 23 Sep 2023 22:04:19 +0200 Subject: [PATCH 12/31] Add module count stats to overview --- views/digitaljs/src/viewers/stats/elements.ts | 14 ++-- views/digitaljs/src/viewers/stats/modules.ts | 70 +++++++++++-------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 0c61eee..7b42c06 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -4,7 +4,8 @@ import {type Module, ModuleStatId, getModuleStatIds, getModuleStatName} from './ const fmtStat = (stat1: number, stat2: number): string => { let res = `${stat1}/${stat2}`; if (stat1 !== 0) { - res += ` (${(stat1 / stat2) * 100}%)`; + const perc = Math.floor((stat1 / stat2) * 10_000) / 100; + res += ` (${perc}%)`; } return res; @@ -166,9 +167,10 @@ export class ModuleOverviewGrid extends DataGrid { throw new Error('Cannot fill header cells!'); } + const rowModule = this.modules[y - 1]; + const moduleCount = this.modules[0].globalChildren.get(rowModule) ?? 0; if (x === 1) { - // TODO: add module count - return super.setCell(x, y, '-'); + return super.setCell(x, y, moduleCount.toString()); } if (!statId) { @@ -189,7 +191,7 @@ export class ModuleOverviewGrid extends DataGrid { const res = super.setCell( x, y, - fmtStat(this.modules[y - 1].globalStats[statId], this.modules[0].globalStats[statId]) + fmtStat(rowModule.globalStats[statId] * moduleCount, this.modules[0].globalStats[statId]) ); this.dispatchEvent('overviewGridStatUpdate', { @@ -323,9 +325,7 @@ export class ModuleExplorerGrid extends DataGrid { // Current module stats this.addRow( ['', ''].concat( - this.moduleStats.map((statId) => - fmtStat(this.curModule.localStats[statId], this.curModule.localStats[statId]) - ) + this.moduleStats.map((statId) => fmtStat(this.curModule.stats[statId], this.curModule.stats[statId])) ) ); diff --git a/views/digitaljs/src/viewers/stats/modules.ts b/views/digitaljs/src/viewers/stats/modules.ts index 71f80a8..d96fc02 100644 --- a/views/digitaljs/src/viewers/stats/modules.ts +++ b/views/digitaljs/src/viewers/stats/modules.ts @@ -29,66 +29,78 @@ export const getModuleStatName = (stat: ModuleStatId): string => { }; export class Module { - private _children: Map; private parents: Set; - private primitiveModules: Map; + private _primitives: Map; public readonly name: string; - public readonly localStats: ModuleStats; - private _globalStats: ModuleStats | null; - constructor(name: string, stats: YosysModuleStats) { - this._children = new Map(); - this.parents = new Set(); - this.primitiveModules = new Map(); + private _children: Map; + private _globalChildren: Map; + + private _stats: ModuleStats; + private _globalStats: ModuleStats; + constructor(name: string, stats: YosysModuleStats) { this.name = name; - this.localStats = { + this._stats = { memoryCount: stats.num_memories, memoryBitCount: stats.num_memory_bits, processCount: stats.num_processes, cellCount: stats.num_cells }; - this._globalStats = null; + this._globalStats = structuredClone(this._stats); + + this._children = new Map(); + this._globalChildren = new Map(); + + this._primitives = new Map(); + + this.parents = new Set(); + } + + get isTopLevel(): boolean { + return this.parents.size === 0; } get primitives(): Map { - return this.primitiveModules; + return this._primitives; } get children(): Map { return this._children; } - get isTopLevel(): boolean { - return this.parents.size === 0; + get globalChildren(): Map { + return this._globalChildren; } - get globalStats(): ModuleStats { - if (this._globalStats) { - return this._globalStats; - } - - this._globalStats = structuredClone(this.localStats); - for (const [module, count] of this._children) { - const moduleStats = module.globalStats; - this._globalStats.memoryCount += count * moduleStats.memoryCount; - this._globalStats.memoryBitCount += count * moduleStats.memoryBitCount; - this._globalStats.cellCount += count * moduleStats.cellCount; - } + get stats(): ModuleStats { + return this._stats; + } + get globalStats(): ModuleStats { return this._globalStats; } addPrimitive(prim: string, count: number) { - const curCount = this.primitiveModules.get(prim) || 0; - this.primitiveModules.set(prim, curCount + count); + this._primitives.set(prim, (this._primitives.get(prim) ?? 0) + count); } addChild(module: Module, count: number) { - const curCount = this._children.get(module) || 0; - this._children.set(module, curCount + count); + console.log(`Add ${module.name} to ${this.name} - x${count}`); + this._children.set(module, (this._children.get(module) ?? 0) + count); module.addParent(this); + + // Update global stats + this._globalStats.memoryCount += count * module.globalStats.memoryCount; + this._globalStats.memoryBitCount += count * module.globalStats.memoryBitCount; + this._globalStats.cellCount += count * module.globalStats.cellCount; + + // Update global children + this._globalChildren.set(module, (this._globalChildren.get(module) ?? 0) + count); + for (const [gloModule, gloCount] of module.globalChildren.entries()) { + this._globalChildren.set(gloModule, (this._globalChildren.get(gloModule) ?? 0) + count * gloCount); + } } protected addParent(module: Module) { From 11b9c20c0c591909df14f9dd22046a7305555828 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 30 Sep 2023 22:46:15 +0200 Subject: [PATCH 13/31] (WIP) Make circuit explorer columns more interactive (also increases overall performance by a LOT) --- views/digitaljs/src/viewers/stats/elements.ts | 386 +++++++++++++----- views/digitaljs/src/viewers/stats/modules.ts | 1 - views/digitaljs/src/viewers/stats/stats.ts | 18 +- 3 files changed, 276 insertions(+), 129 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 7b42c06..7aaccc6 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -1,5 +1,5 @@ import type {CustomEventListener, CustomEvents} from './events'; -import {type Module, ModuleStatId, getModuleStatIds, getModuleStatName} from './modules'; +import {type Module, type ModuleStatId, getModuleStatIds, getModuleStatName} from './modules'; const fmtStat = (stat1: number, stat2: number): string => { let res = `${stat1}/${stat2}`; @@ -151,62 +151,84 @@ export class DataGrid extends CustomElement { } } -export class ModuleOverviewGrid extends DataGrid { - private readonly modules: Module[]; +abstract class InteractiveDataGrid extends DataGrid { + private rows: RowItem[]; + private cols: ColumnOption[]; - constructor(modules: Module[]) { + private actualRoot: HTMLElement; + + constructor() { super(); - this.modules = modules; + this.rows = []; + this.cols = []; + + // The object renders to this.rootElem, but we want buttons outside + // the datagrid. This actualRoot is just a div containing those buttons + // and the rendered rootElem so we can 'adopt' the buttons into this class. + this.actualRoot = this.createRoot(); - this.reset(); + this.reset(true, true); } - private fillCell(x: number, y: number, statId?: ModuleStatId) { - if (x === 0 || y === 0) { - throw new Error('Cannot fill header cells!'); - } + override get element(): HTMLElement { + return this.actualRoot; + } - const rowModule = this.modules[y - 1]; - const moduleCount = this.modules[0].globalChildren.get(rowModule) ?? 0; - if (x === 1) { - return super.setCell(x, y, moduleCount.toString()); - } + protected abstract getDefaultOptions(): ColumnOption[]; - if (!statId) { - // Find active column stat id - const header = this.cells[0][x]; - if (!(header instanceof Element)) { - return super.setCell(x, y, '-'); - } - const dropdown = header.querySelector('vscode-dropdown'); - const activeId = dropdown?.getAttribute('aria-activedescendant'); - const activeElem = dropdown?.querySelector(`#${activeId}`); - statId = activeElem?.getAttribute('stat-id') as ModuleStatId; - if (!statId) { - return; - } - } + protected abstract getAvailableOptions(): ColumnOption[]; + + protected abstract getNewOption(): ColumnOption; - const res = super.setCell( - x, - y, - fmtStat(rowModule.globalStats[statId] * moduleCount, this.modules[0].globalStats[statId]) - ); + protected abstract getOptionName(option: ColumnOption): string; - this.dispatchEvent('overviewGridStatUpdate', { - element: this, - data: {index: x - 2, statId} + protected abstract getValue(item: RowItem, option: ColumnOption): DataGridCell; + + private createRoot() { + const root = document.createElement('div'); + + // Add column button + const addColBtn = document.createElement('vscode-button'); + addColBtn.innerHTML = /* html */ ``; + addColBtn.addEventListener('click', (_ev) => { + this.addCol(); + }); + root.appendChild(addColBtn); + + // Reset button + const resetBtn = document.createElement('vscode-button'); + resetBtn.innerHTML = /* html */ ``; + resetBtn.addEventListener('click', (_ev) => { + this.reset(true, false); }); + root.appendChild(resetBtn); + + // The actual DataGrid + root.appendChild(super.element); - return res; + return root; } - addRow(contents: DataGridCell[], pos?: number): number { - const y = super.addRow(contents, pos); + protected fillCell(x: number, y: number) { + const defColsLen = this.getDefaultOptions().length; + if (x < 0 || x >= defColsLen + this.cols.length || y < 1 || y - 1 >= this.rows.length) { + throw new Error('Out of bounds!'); + } + + const option = this.getDefaultOptions().concat(this.cols)[x]; + const item = this.rows[y - 1]; + const value = this.getValue(item, option); + + return super.setCell(x, y, value); + } + + addRowItem(item: RowItem): number { + this.rows.push(item); + const y = super.addRow([]); if (y !== 0) { - for (let x = 1; x < this.width; x++) { + for (let x = 0; x < this.width; x++) { this.fillCell(x, y); } } @@ -214,76 +236,83 @@ export class ModuleOverviewGrid extends DataGrid { return y; } - addStat(statId?: ModuleStatId): number { + addCol(option?: ColumnOption): number { + if (option === undefined) { + option = this.getNewOption(); + } + this.cols.push(option); + const header = document.createElement('div'); const delBtn = document.createElement('vscode-button'); delBtn.innerHTML = /* html */ ``; delBtn.addEventListener('click', (_ev) => { const coli = this.cells[0].indexOf(header); - if (coli !== -1) { + const defColCount = this.getDefaultOptions().length; + if (coli >= defColCount) { + this.cols.splice(coli - defColCount, 1); this.delColumn(coli); this.render(); } }); header.appendChild(delBtn); - const dropdown = document.createElement('vscode-dropdown'); - const addOption = (id: ModuleStatId) => { + const dropdown = document.createElement('vscode-dropdown') as HTMLSelectElement; + for (const avOption of this.getAvailableOptions()) { const opt = document.createElement('vscode-option'); - opt.textContent = getModuleStatName(id); - opt.setAttribute('stat-id', id); - dropdown.appendChild(opt); - }; - - if (statId) { - addOption(statId); - } - for (const id of getModuleStatIds()) { - if (id !== statId) { - addOption(id); + opt.textContent = this.getOptionName(avOption); + if (avOption === option) { + opt.setAttribute('selected', ''); } + dropdown.appendChild(opt); } - header.appendChild(dropdown); - const pos = super.addColumn([header]); + dropdown.addEventListener('change', (_ev) => { + const headerIndex = this.cells[0].indexOf(header); + if (headerIndex === -1) return; + + const newOption = this.getAvailableOptions()[dropdown.selectedIndex]; + + // Actually update the column and re-render + this.cols.splice(headerIndex - this.getDefaultOptions().length, 1, newOption); + this.render(); + }); - // Render after 100ms, the dropdown isn't updated immediately - header.addEventListener('change', (_ev) => setTimeout(this.render.bind(this), 100)); + const pos = super.addColumn([header]); - // Adds the new column into the table this.render(); - // Fill column cells with default values for (let y = 1; y < this.height; y++) { - this.fillCell(pos, y, Object.values(ModuleStatId)[0]); + this.fillCell(pos, y); } return pos; } - reset() { + reset(resetCols: boolean, resetRows: boolean) { + if (resetCols) this.cols = []; + if (resetRows) this.rows = []; + this.clearRows(); while (this.width > 0) { this.delColumn(0); } - super.addColumn(['Module Name']); - super.addColumn(['Count']); + for (const option of this.getDefaultOptions()) { + super.addColumn([this.getOptionName(option)]); + } - setTimeout(this.render.bind(this), 100); + this.render(); } render() { this.clearRows(); - for (const module of this.modules) { - this.addRow([module.name + (module.isTopLevel ? ' (top-level)' : '')]); - } + this.rows.forEach(() => super.addRow([])); for (let y = 1; y < this.height; y++) { - for (let x = 1; x < this.width; x++) { + for (let x = 0; x < this.width; x++) { this.fillCell(x, y); } } @@ -292,68 +321,203 @@ export class ModuleOverviewGrid extends DataGrid { } } -export class ModuleExplorerGrid extends DataGrid { - private curModule: Module; +type ModuleOverviewOptions = 'name' | 'count' | ModuleStatId; - private moduleStats: ModuleStatId[]; +export class ModuleOverviewGrid extends InteractiveDataGrid { + private modules: Module[]; + + constructor(modules: Module[]) { + super(); + + this.modules = modules; + + for (const module of modules) { + this.addRowItem(module); + } + } + + protected getDefaultOptions(): ModuleOverviewOptions[] { + return ['name']; + } + + protected getAvailableOptions(): ModuleOverviewOptions[] { + return getModuleStatIds(); + } + + protected getNewOption(): ModuleOverviewOptions { + return this.getAvailableOptions()[0]; + } + + protected getOptionName(option: ModuleOverviewOptions): string { + switch (option) { + case 'name': + return 'Module name'; + case 'count': + return 'Count'; + default: + return getModuleStatName(option); + } + } + + protected getValue(item: Module, option: ModuleOverviewOptions): DataGridCell { + switch (option) { + case 'name': + return item.name; + case 'count': + return this.modules[0].globalChildren.get(item)?.toString() ?? '-'; + default: + return fmtStat(item.globalStats[option], this.modules[0].globalStats[option]); + } + } +} + +interface ModuleExplorerRowCurrent { + type: 'current'; +} +interface ModuleExplorerRowPrimitive { + type: 'primitive'; + primitive: string; +} +interface ModuleExplorerRowChild { + type: 'child'; + module: Module; +} +type ModuleExplorerRowItems = ModuleExplorerRowCurrent | ModuleExplorerRowPrimitive | ModuleExplorerRowChild; +type ModuleExplorerOptions = 'name' | 'count' | ModuleStatId; + +export class ModuleExplorerGrid extends InteractiveDataGrid { + private curModule: Module; constructor(initModule: Module) { super(); this.curModule = initModule; - this.moduleStats = []; - this.addColumn(['Module Name']); - this.addColumn(['Count']); + this.setModule(this.curModule); } setModule(module: Module) { this.curModule = module; + + this.reset(false, true); + + this.addRowItem({type: 'current'}); + for (const prim of this.curModule.primitives.keys()) { + this.addRowItem({type: 'primitive', primitive: prim}); + } + for (const subCircuit of this.curModule.children.keys()) { + this.addRowItem({type: 'child', module: subCircuit}); + } } - addStat(stat: ModuleStatId) { - if (this.moduleStats.indexOf(stat) !== -1) { - return; + protected getDefaultOptions(): ModuleExplorerOptions[] { + return ['name', 'count']; + } + + protected getAvailableOptions(): ModuleExplorerOptions[] { + return getModuleStatIds(); + } + + protected getNewOption(): ModuleExplorerOptions { + return this.getAvailableOptions()[0]; + } + + protected getOptionName(option: ModuleOverviewOptions): string { + switch (option) { + case 'name': + return 'Module name'; + case 'count': + return 'Count'; + default: + return getModuleStatName(option); } - this.moduleStats.push(stat); - this.addColumn([getModuleStatName(stat)]); } - render() { - this.clearRows(); + protected getValue(item: ModuleExplorerRowItems, option: ModuleOverviewOptions): DataGridCell { + switch (item.type) { + case 'current': + return this.getValueCurrent(item, option); + case 'primitive': + return this.getValuePrimitive(item, option); + case 'child': + return this.getValueChild(item, option); + } + } - // Current module stats - this.addRow( - ['', ''].concat( - this.moduleStats.map((statId) => fmtStat(this.curModule.stats[statId], this.curModule.stats[statId])) - ) - ); - - // Primitives - const rowFiller: string[] = new Array(this.width - 2).map((_) => ''); - for (const [prim, count] of this.curModule.primitives.entries()) { - this.addRow([`$${prim}`, count.toString()].concat(rowFiller)); + private getValueCurrent(_item: ModuleExplorerRowCurrent, option: ModuleOverviewOptions): DataGridCell { + switch (option) { + case 'name': + return '< Current Module >'; + case 'count': + return '-'; + default: + return '[TODO]'; // TODO } + } - // Child modules - for (const [child, count] of this.curModule.children.entries()) { - const link = document.createElement('vscode-link'); - link.textContent = child.name; - link.addEventListener('click', (_ev) => { - this.dispatchEvent('explorerModuleClicked', {element: this, data: {module: child}}); - }); - - this.addRow( - [link, count.toString()].concat( - this.moduleStats.map((statId) => - fmtStat(child.globalStats[statId] * count, this.curModule.globalStats[statId]) - ) - ) - ); + private getValuePrimitive(item: ModuleExplorerRowPrimitive, option: ModuleOverviewOptions): DataGridCell { + switch (option) { + case 'name': + return item.primitive; + case 'count': + return this.curModule.primitives.get(item.primitive)?.toString() ?? '-'; + default: + return '[TODO]'; // TODO } + } - super.render(); + private getValueChild(item: ModuleExplorerRowChild, option: ModuleOverviewOptions): DataGridCell { + switch (option) { + case 'name': { + const link = document.createElement('vscode-link'); + link.textContent = item.module.name; + link.addEventListener('click', (_ev) => { + this.dispatchEvent('explorerModuleClicked', {element: this, data: {module: item.module}}); + }); + return link; + } + case 'count': + return this.curModule.children.get(item.module)?.toString() ?? '-'; + default: + return '[TODO]'; // TODO + } } + + // render() { + // this.clearRows(); + + // // Current module stats + // this.addRow( + // ['', ''].concat( + // this.moduleStats.map((statId) => fmtStat(this.curModule.stats[statId], this.curModule.stats[statId])) + // ) + // ); + + // // Primitives + // const rowFiller: string[] = new Array(this.width - 2).map((_) => ''); + // for (const [prim, count] of this.curModule.primitives.entries()) { + // this.addRow([`$${prim}`, count.toString()].concat(rowFiller)); + // } + + // // Child modules + // for (const [child, count] of this.curModule.children.entries()) { + // const link = document.createElement('vscode-link'); + // link.textContent = child.name; + // link.addEventListener('click', (_ev) => { + // this.dispatchEvent('explorerModuleClicked', {element: this, data: {module: child}}); + // }); + + // this.addRow( + // [link, count.toString()].concat( + // this.moduleStats.map((statId) => + // fmtStat(child.globalStats[statId] * count, this.curModule.globalStats[statId]) + // ) + // ) + // ); + // } + + // super.render(); + // } } export class ModuleNavigator extends CustomElement { diff --git a/views/digitaljs/src/viewers/stats/modules.ts b/views/digitaljs/src/viewers/stats/modules.ts index d96fc02..07f6161 100644 --- a/views/digitaljs/src/viewers/stats/modules.ts +++ b/views/digitaljs/src/viewers/stats/modules.ts @@ -87,7 +87,6 @@ export class Module { } addChild(module: Module, count: number) { - console.log(`Add ${module.name} to ${this.name} - x${count}`); this._children.set(module, (this._children.get(module) ?? 0) + count); module.addParent(this); diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index e438929..4ac54f6 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -68,7 +68,7 @@ export class StatsViewer extends BaseViewer { } for (const col of this.overviewSettings.columns) { console.log(`Add ${col}`); - this.overviewGrid.addStat(col); + this.overviewGrid.addCol(col); } }); @@ -112,22 +112,6 @@ export class StatsViewer extends BaseViewer { overviewHeader.textContent = 'Circuit overview'; this.root.appendChild(overviewHeader); - const addColBtn = document.createElement('vscode-button'); - addColBtn.innerHTML = /* html */ ``; - addColBtn.addEventListener('click', (_ev) => { - this.overviewGrid.addStat(); - }); - this.root.appendChild(addColBtn); - - const resetBtn = document.createElement('vscode-button'); - resetBtn.innerHTML = /* html */ ``; - resetBtn.addEventListener('click', (_ev) => { - this.overviewGrid.reset(); - }); - this.root.appendChild(resetBtn); - - this.root.appendChild(document.createElement('br')); - this.overviewGrid.render(); this.root.appendChild(this.overviewGrid.element); From b5966a2668402751e3714f21d245e9a32a6b0f0b Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 7 Oct 2023 14:44:29 +0200 Subject: [PATCH 14/31] Show actual module stats in explorer --- views/digitaljs/src/viewers/stats/elements.ts | 66 +++++-------------- 1 file changed, 18 insertions(+), 48 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 7aaccc6..5e9348f 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -1,14 +1,12 @@ import type {CustomEventListener, CustomEvents} from './events'; import {type Module, type ModuleStatId, getModuleStatIds, getModuleStatName} from './modules'; -const fmtStat = (stat1: number, stat2: number): string => { - let res = `${stat1}/${stat2}`; - if (stat1 !== 0) { - const perc = Math.floor((stat1 / stat2) * 10_000) / 100; - res += ` (${perc}%)`; +const getPercentage = (val1: number, val2: number): number => { + if (val1 === 0 || val2 === 0) { + return 0; } - return res; + return Math.floor((val1 / val2) * 10_000) / 100; }; export abstract class CustomElement { @@ -365,8 +363,12 @@ export class ModuleOverviewGrid extends InteractiveDataGrid', ''].concat( - // this.moduleStats.map((statId) => fmtStat(this.curModule.stats[statId], this.curModule.stats[statId])) - // ) - // ); - - // // Primitives - // const rowFiller: string[] = new Array(this.width - 2).map((_) => ''); - // for (const [prim, count] of this.curModule.primitives.entries()) { - // this.addRow([`$${prim}`, count.toString()].concat(rowFiller)); - // } - - // // Child modules - // for (const [child, count] of this.curModule.children.entries()) { - // const link = document.createElement('vscode-link'); - // link.textContent = child.name; - // link.addEventListener('click', (_ev) => { - // this.dispatchEvent('explorerModuleClicked', {element: this, data: {module: child}}); - // }); - - // this.addRow( - // [link, count.toString()].concat( - // this.moduleStats.map((statId) => - // fmtStat(child.globalStats[statId] * count, this.curModule.globalStats[statId]) - // ) - // ) - // ); - // } - - // super.render(); - // } } export class ModuleNavigator extends CustomElement { From 535a64e50a9404004a62dc21825b690d6184dd5a Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 7 Oct 2023 16:22:51 +0200 Subject: [PATCH 15/31] Make events scope specific to elements --- views/digitaljs/src/viewers/stats/elements.ts | 34 +++++++++++----- views/digitaljs/src/viewers/stats/events.ts | 34 ---------------- views/digitaljs/src/viewers/stats/stats.ts | 40 ++++++------------- 3 files changed, 35 insertions(+), 73 deletions(-) delete mode 100644 views/digitaljs/src/viewers/stats/events.ts diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 5e9348f..134e5fb 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -1,4 +1,3 @@ -import type {CustomEventListener, CustomEvents} from './events'; import {type Module, type ModuleStatId, getModuleStatIds, getModuleStatName} from './modules'; const getPercentage = (val1: number, val2: number): number => { @@ -9,7 +8,7 @@ const getPercentage = (val1: number, val2: number): number => { return Math.floor((val1 / val2) * 10_000) / 100; }; -export abstract class CustomElement { +export abstract class CustomElement { protected abstract rootElem: HTMLElement; private eventsElem: Element; @@ -18,11 +17,11 @@ export abstract class CustomElement { this.eventsElem = document.createElement('events'); } - addEventListener(type: K, listener: CustomEventListener) { + addEventListener(type: K, listener: (ev: EventsDirectory[K]) => void) { this.eventsElem.addEventListener(type, (ev: CustomEventInit) => listener(ev.detail)); } - protected dispatchEvent(type: K, data: CustomEvents[K]) { + protected dispatchEvent(type: K, data: EventsDirectory[K]) { this.eventsElem.dispatchEvent(new CustomEvent(type, {detail: data})); } @@ -35,7 +34,7 @@ export abstract class CustomElement { type DataGridCell = Node | string; -export class DataGrid extends CustomElement { +export class DataGrid extends CustomElement { protected rootElem: HTMLElement; protected cells: DataGridCell[][]; @@ -76,10 +75,6 @@ export class DataGrid extends CustomElement { for (const row of this.cells) { row.splice(pos, 1); } - - if (pos >= 2) { - this.dispatchEvent('overviewGridStatUpdate', {element: this, data: {index: pos - 2, statId: null}}); - } } addRow(contents: DataGridCell[], pos?: number): number { @@ -149,7 +144,14 @@ export class DataGrid extends CustomElement { } } -abstract class InteractiveDataGrid extends DataGrid { +interface GridHeadersUpdateEvent { + newHeaders: ColumnOption[]; +} +interface InteractiveDataGridEvents { + gridHeadersUpdate: GridHeadersUpdateEvent; +} + +abstract class InteractiveDataGrid extends DataGrid> { private rows: RowItem[]; private cols: ColumnOption[]; @@ -191,6 +193,8 @@ abstract class InteractiveDataGrid extends DataGrid { addColBtn.innerHTML = /* html */ ``; addColBtn.addEventListener('click', (_ev) => { this.addCol(); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); }); root.appendChild(addColBtn); @@ -199,6 +203,8 @@ abstract class InteractiveDataGrid extends DataGrid { resetBtn.innerHTML = /* html */ ``; resetBtn.addEventListener('click', (_ev) => { this.reset(true, false); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); }); root.appendChild(resetBtn); @@ -251,6 +257,8 @@ abstract class InteractiveDataGrid extends DataGrid { this.cols.splice(coli - defColCount, 1); this.delColumn(coli); this.render(); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); } }); header.appendChild(delBtn); @@ -275,6 +283,8 @@ abstract class InteractiveDataGrid extends DataGrid { // Actually update the column and re-render this.cols.splice(headerIndex - this.getDefaultOptions().length, 1, newOption); this.render(); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); }); const pos = super.addColumn([header]); @@ -474,6 +484,7 @@ export class ModuleExplorerGrid extends InteractiveDataGrid { + //@ts-expect-error: TODO this.dispatchEvent('explorerModuleClicked', {element: this, data: {module: item.module}}); }); return link; @@ -490,7 +501,7 @@ export class ModuleExplorerGrid extends InteractiveDataGrid> { protected rootElem: HTMLElement; private moduleBreadcrumbs: Module[]; @@ -508,6 +519,7 @@ export class ModuleNavigator extends CustomElement { navigateSplice(i: number) { this.moduleBreadcrumbs.splice(i + 1); + //@ts-expect-error: TODO this.dispatchEvent('explorerFocusUpdate', {element: this, data: {module: this.moduleBreadcrumbs[i]}}); } diff --git a/views/digitaljs/src/viewers/stats/events.ts b/views/digitaljs/src/viewers/stats/events.ts deleted file mode 100644 index 6bff4a1..0000000 --- a/views/digitaljs/src/viewers/stats/events.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type {CustomElement} from './elements'; -import type {Module, ModuleStatId} from './modules'; - -interface OverviewStatUpdateEvent { - index: number; - statId: ModuleStatId | null; -} - -interface ModuleClickedEvent { - module: Module; -} - -interface ExplorerFocusUpdateEvent { - module: Module; -} - -interface CheckboxClickedEvent { - id: ModuleStatId; - checked: boolean; -} - -interface CustomEvent { - element: CustomElement; - data: E; -} - -export interface CustomEvents { - overviewGridStatUpdate: CustomEvent; - explorerModuleClicked: CustomEvent; - explorerFocusUpdate: CustomEvent; - checkboxClicked: CustomEvent; -} - -export type CustomEventListener = (ev: CustomEvents[K]) => void; diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index 4ac54f6..f83c527 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -32,7 +32,6 @@ export class StatsViewer extends BaseViewer { private modules: Module[]; - private overviewSettings: OverviewGridSettings; private overviewGrid: ModuleOverviewGrid; private explorerNavigator: ModuleNavigator; @@ -47,41 +46,26 @@ export class StatsViewer extends BaseViewer { } this.overviewGrid = new ModuleOverviewGrid(this.modules); - this.overviewGrid.addEventListener('overviewGridStatUpdate', (event) => { - if (this.overviewSettings.columns[event.data.index] === event.data.statId) { - return; - } - if (event.data.statId === null) { - this.overviewSettings.columns.splice(event.data.index, 1); - } else { - this.overviewSettings.columns[event.data.index] = event.data.statId; - } - this.storeValue('djs-stats-overview-settings', this.overviewSettings); + this.overviewGrid.addEventListener('gridHeadersUpdate', (data) => { + this.storeValue('yosys-stats-overview-settings', {columns: data.newHeaders}); }); - - this.overviewSettings = {columns: []}; - this.getValue('djs-stats-overview-settings').then((value) => { - if (Object.keys(value).length === 0) { - this.overviewSettings = {columns: []}; - } else { - this.overviewSettings = value as OverviewGridSettings; - } - for (const col of this.overviewSettings.columns) { - console.log(`Add ${col}`); + this.getValue('yosys-stats-overview-settings').then((value) => { + const settings = Object.keys(value).length ? (value as OverviewGridSettings) : {columns: []}; + for (const col of settings.columns) { this.overviewGrid.addCol(col); } }); this.explorerNavigator = new ModuleNavigator(this.modules[0]); - this.explorerNavigator.addEventListener('explorerFocusUpdate', (event) => { - this.renderExplorer(event.data.module); - }); + // this.explorerNavigator.addEventListener('explorerFocusUpdate', (event) => { + // this.renderExplorer(event.data.module); + // }); this.explorerGrid = new ModuleExplorerGrid(this.modules[0]); - this.explorerGrid.addEventListener('explorerModuleClicked', (event) => { - this.explorerNavigator.navigateModule(event.data.module); - this.renderExplorer(event.data.module); - }); + // this.explorerGrid.addEventListener('explorerModuleClicked', (event) => { + // this.explorerNavigator.navigateModule(event.data.module); + // this.renderExplorer(event.data.module); + // }); } private getModule(name: string): Module | null { From df56190cc2188d867f4a7d813eb08badbd272b54 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 7 Oct 2023 17:20:52 +0200 Subject: [PATCH 16/31] Adopt breadcrumbs header into module explorer element --- views/digitaljs/src/viewers/stats/elements.ts | 101 ++++++++++-------- views/digitaljs/src/viewers/stats/stats.ts | 64 +++++------ 2 files changed, 80 insertions(+), 85 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 134e5fb..9d2560d 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -398,21 +398,66 @@ type ModuleExplorerRowItems = ModuleExplorerRowCurrent | ModuleExplorerRowPrimit type ModuleExplorerOptions = 'name' | 'count' | ModuleStatId; export class ModuleExplorerGrid extends InteractiveDataGrid { - private curModule: Module; + private actualRoot2: HTMLElement; + private breadcrumbHeader: HTMLParagraphElement; + + private moduleBreadcrumbs: Module[]; constructor(initModule: Module) { super(); - this.curModule = initModule; + this.breadcrumbHeader = document.createElement('p'); + this.actualRoot2 = document.createElement('div'); + this.actualRoot2.appendChild(this.breadcrumbHeader); + this.actualRoot2.appendChild(super.element); + + this.moduleBreadcrumbs = [initModule]; - this.setModule(this.curModule); + this.update(); } - setModule(module: Module) { - this.curModule = module; + override get element(): HTMLElement { + return this.actualRoot2; + } - this.reset(false, true); + get curModule(): Module { + return this.moduleBreadcrumbs[this.moduleBreadcrumbs.length - 1]; + } + + navigateSplice(i: number) { + if (i === 0) { + throw new Error('Cannot navigate above top-level module!'); + } + this.moduleBreadcrumbs.splice(i); + } + + navigate(module: Module) { + if (Array.from(this.curModule.children.keys()).indexOf(module) === -1) { + throw new Error(`Cannot navigate to module "${module.name}": not a child of "${this.curModule.name}"`); + } + this.moduleBreadcrumbs.push(module); + } + + update() { + // Update breadcrumbs + this.breadcrumbHeader.replaceChildren(); + + for (let i = 0; i < this.moduleBreadcrumbs.length; i++) { + const link = document.createElement('vscode-link'); + link.textContent = this.moduleBreadcrumbs[i].name; + link.addEventListener('click', (_ev) => { + this.navigateSplice(i + 1); + this.update(); + }); + this.breadcrumbHeader.append(link); + if (i != this.moduleBreadcrumbs.length - 1) { + this.breadcrumbHeader.append(' > '); + } + } + + // Update grid + this.reset(false, true); this.addRowItem({type: 'current'}); for (const prim of this.curModule.primitives.keys()) { this.addRowItem({type: 'primitive', primitive: prim}); @@ -420,6 +465,8 @@ export class ModuleExplorerGrid extends InteractiveDataGrid { - //@ts-expect-error: TODO - this.dispatchEvent('explorerModuleClicked', {element: this, data: {module: item.module}}); + this.navigate(item.module); + this.update(); }); return link; } @@ -500,41 +547,3 @@ export class ModuleExplorerGrid extends InteractiveDataGrid> { - protected rootElem: HTMLElement; - - private moduleBreadcrumbs: Module[]; - - constructor(initModule: Module) { - super(); - - this.rootElem = document.createElement('p'); - this.moduleBreadcrumbs = [initModule]; - } - - navigateModule(module: Module) { - this.moduleBreadcrumbs.push(module); - } - - navigateSplice(i: number) { - this.moduleBreadcrumbs.splice(i + 1); - //@ts-expect-error: TODO - this.dispatchEvent('explorerFocusUpdate', {element: this, data: {module: this.moduleBreadcrumbs[i]}}); - } - - render(): void { - this.rootElem.replaceChildren(); - - for (let i = 0; i < this.moduleBreadcrumbs.length; i++) { - const link = document.createElement('vscode-link'); - link.textContent = this.moduleBreadcrumbs[i].name; - link.addEventListener('click', (_ev) => this.navigateSplice(i)); - this.rootElem.append(link); - - if (i != this.moduleBreadcrumbs.length - 1) { - this.rootElem.append(' > '); - } - } - } -} diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index f83c527..9578afa 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -3,10 +3,10 @@ import type {ForeignViewMessage} from '../../messages'; import type {YosysStats} from '../../types'; import {BaseViewer} from '../base'; -import {ModuleExplorerGrid, ModuleNavigator, ModuleOverviewGrid} from './elements'; +import {ModuleExplorerGrid, ModuleOverviewGrid} from './elements'; import {type Module, type ModuleStatId, buildModuleTree} from './modules'; -interface OverviewGridSettings { +interface GridColumnSettings { columns: ModuleStatId[]; } @@ -14,28 +14,23 @@ export class StatsViewer extends BaseViewer { handleForeignViewMessage(message: ForeignViewMessage): void { if (message.type === 'moduleFocus') { // Focus explorer navigator according to breadcrumbs from djs viewer - this.explorerNavigator.navigateSplice(0); - let lastModule: Module = this.modules[0]; + this.moduleExplorer.navigateSplice(1); for (const moduleName of message.breadcrumbs) { const module = this.getModule(moduleName); if (!module) { throw new Error(`Unknown module: ${moduleName}`); } - this.explorerNavigator.navigateModule(module); - - lastModule = module; + this.moduleExplorer.navigate(module); } - this.renderExplorer(lastModule); + this.moduleExplorer.update(); } } private modules: Module[]; - private overviewGrid: ModuleOverviewGrid; - - private explorerNavigator: ModuleNavigator; - private explorerGrid: ModuleExplorerGrid; + private moduleOverview: ModuleOverviewGrid; + private moduleExplorer: ModuleExplorerGrid; constructor(mainView: View, initData: YosysStats) { super(mainView, initData); @@ -45,27 +40,27 @@ export class StatsViewer extends BaseViewer { throw new Error('No circuit modules found to display!'); } - this.overviewGrid = new ModuleOverviewGrid(this.modules); - this.overviewGrid.addEventListener('gridHeadersUpdate', (data) => { + this.moduleOverview = new ModuleOverviewGrid(this.modules); + this.moduleOverview.addEventListener('gridHeadersUpdate', (data) => { this.storeValue('yosys-stats-overview-settings', {columns: data.newHeaders}); }); this.getValue('yosys-stats-overview-settings').then((value) => { - const settings = Object.keys(value).length ? (value as OverviewGridSettings) : {columns: []}; + const settings = Object.keys(value).length ? (value as GridColumnSettings) : {columns: []}; for (const col of settings.columns) { - this.overviewGrid.addCol(col); + this.moduleOverview.addCol(col); } }); - this.explorerNavigator = new ModuleNavigator(this.modules[0]); - // this.explorerNavigator.addEventListener('explorerFocusUpdate', (event) => { - // this.renderExplorer(event.data.module); - // }); - - this.explorerGrid = new ModuleExplorerGrid(this.modules[0]); - // this.explorerGrid.addEventListener('explorerModuleClicked', (event) => { - // this.explorerNavigator.navigateModule(event.data.module); - // this.renderExplorer(event.data.module); - // }); + this.moduleExplorer = new ModuleExplorerGrid(this.modules[0]); + this.moduleExplorer.addEventListener('gridHeadersUpdate', (data) => { + this.storeValue('yosys-stats-explorer-settings', {columns: data.newHeaders}); + }); + this.getValue('yosys-stats-explorer-settings').then((value) => { + const settings = Object.keys(value).length ? (value as GridColumnSettings) : {columns: []}; + for (const col of settings.columns) { + this.moduleExplorer.addCol(col); + } + }); } private getModule(name: string): Module | null { @@ -77,13 +72,6 @@ export class StatsViewer extends BaseViewer { return null; } - private renderExplorer(module: Module) { - this.explorerGrid.setModule(module); - this.explorerGrid.render(); - - this.explorerNavigator.render(); - } - async render(isUpdate: boolean): Promise { if (isUpdate) { // All elements are dynamically resized so we don't need to redraw @@ -96,8 +84,8 @@ export class StatsViewer extends BaseViewer { overviewHeader.textContent = 'Circuit overview'; this.root.appendChild(overviewHeader); - this.overviewGrid.render(); - this.root.appendChild(this.overviewGrid.element); + this.moduleOverview.render(); + this.root.appendChild(this.moduleOverview.element); // ** Divider ** this.root.appendChild(document.createElement('br')); @@ -108,9 +96,7 @@ export class StatsViewer extends BaseViewer { explorerHeader.textContent = 'Circuit explorer'; this.root.appendChild(explorerHeader); - this.explorerNavigator.render(); - this.root.appendChild(this.explorerNavigator.element); - this.explorerGrid.render(); - this.root.appendChild(this.explorerGrid.element); + this.moduleExplorer.render(); + this.root.appendChild(this.moduleExplorer.element); } } From 1a04c118ee75b8e09bcca976afccf5835c75af1c Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 7 Oct 2023 17:39:44 +0200 Subject: [PATCH 17/31] Fix columns disappearing when switching between modules --- views/digitaljs/src/viewers/stats/elements.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 9d2560d..294e9d4 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -240,11 +240,13 @@ abstract class InteractiveDataGrid extends DataGrid extends DataGrid 0) { this.delColumn(0); } + // Restore them again for (const option of this.getDefaultOptions()) { super.addColumn([this.getOptionName(option)]); } + for (const col of this.cols) { + this.addCol(col, true); + } this.render(); } From 61d8cd71945b208f5e1a3cd3ccfbf893a15eaacc Mon Sep 17 00:00:00 2001 From: Mike A Date: Sun, 8 Oct 2023 14:32:42 +0200 Subject: [PATCH 18/31] Use tabs instead of vertical stacking --- src/extension/editors/project.ts | 2 - views/digitaljs/src/viewers/stats/elements.ts | 44 +++++++++++++++++++ views/digitaljs/src/viewers/stats/stats.ts | 32 ++++++-------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/extension/editors/project.ts b/src/extension/editors/project.ts index 096a192..64506e0 100644 --- a/src/extension/editors/project.ts +++ b/src/extension/editors/project.ts @@ -42,8 +42,6 @@ export class ProjectEditor extends BaseEditor { if (super.onDidReceiveMessage(document, webview, message)) { return true; } - console.log(message); - if (message.type === 'ready') { const project = this.projects.get(document.uri); diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 294e9d4..16e0e7f 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -554,3 +554,47 @@ export class ModuleExplorerGrid extends InteractiveDataGrid { + title: string; + element: CustomElement; +} + +export class TabsContainer extends CustomElement> { + protected rootElem: HTMLElement; + + private tabs: Tab[]; + + constructor(tabs: Tab[]) { + super(); + + this.tabs = tabs; + + this.rootElem = this.createRoot(); + } + + private createRoot(): HTMLElement { + const root = document.createElement('vscode-panels'); + + for (let i = 0; i < this.tabs.length; i++) { + const tab = document.createElement('vscode-panel-tab'); + tab.id = `tab-${i}`; + tab.textContent = this.tabs[i].title; + root.appendChild(tab); + } + for (let i = 0; i < this.tabs.length; i++) { + const view = document.createElement('vscode-panel-view'); + view.id = `view-${i}`; + view.appendChild(this.tabs[i].element.element); + root.appendChild(view); + } + + return root; + } + + render(): void { + for (const tab of this.tabs) { + tab.element.render(); + } + } +} diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index 9578afa..3e7c1ae 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -3,7 +3,7 @@ import type {ForeignViewMessage} from '../../messages'; import type {YosysStats} from '../../types'; import {BaseViewer} from '../base'; -import {ModuleExplorerGrid, ModuleOverviewGrid} from './elements'; +import {ModuleExplorerGrid, ModuleOverviewGrid, TabsContainer} from './elements'; import {type Module, type ModuleStatId, buildModuleTree} from './modules'; interface GridColumnSettings { @@ -29,6 +29,8 @@ export class StatsViewer extends BaseViewer { private modules: Module[]; + private tabsContainer: TabsContainer; + private moduleOverview: ModuleOverviewGrid; private moduleExplorer: ModuleExplorerGrid; @@ -61,6 +63,11 @@ export class StatsViewer extends BaseViewer { this.moduleExplorer.addCol(col); } }); + + this.tabsContainer = new TabsContainer([ + {title: 'Overview', element: this.moduleOverview}, + {title: 'Explorer', element: this.moduleExplorer} + ]); } private getModule(name: string): Module | null { @@ -79,24 +86,11 @@ export class StatsViewer extends BaseViewer { } this.root.replaceChildren(); - // ** Overview Table ** - const overviewHeader = document.createElement('h2'); - overviewHeader.textContent = 'Circuit overview'; - this.root.appendChild(overviewHeader); - - this.moduleOverview.render(); - this.root.appendChild(this.moduleOverview.element); - - // ** Divider ** - this.root.appendChild(document.createElement('br')); - this.root.appendChild(document.createElement('vscode-divider')); - - // ** Circuit explorer ** - const explorerHeader = document.createElement('h2'); - explorerHeader.textContent = 'Circuit explorer'; - this.root.appendChild(explorerHeader); + const header = document.createElement('h1'); + header.textContent = 'Module Statistics'; + this.root.appendChild(header); - this.moduleExplorer.render(); - this.root.appendChild(this.moduleExplorer.element); + this.root.appendChild(this.tabsContainer.element); + this.tabsContainer.render(); } } From 8a48e234bb959c466060b6c70ad638a3d8a60d84 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 14 Oct 2023 20:15:11 +0200 Subject: [PATCH 19/31] Add primitives tab to stats viewer --- views/digitaljs/src/viewers/stats/elements.ts | 74 +++++++++++++++++++ views/digitaljs/src/viewers/stats/modules.ts | 26 +++++-- views/digitaljs/src/viewers/stats/stats.ts | 8 +- 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 16e0e7f..167bb36 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -555,6 +555,80 @@ export class ModuleExplorerGrid extends InteractiveDataGrid { + private modules: Module[]; + + constructor(modules: Module[]) { + super(); + + this.modules = modules; + + this.update(); + + for (let i = 0; i < modules.length && i < 5; i++) { + this.addCol(modules[i]); + } + } + + protected getDefaultOptions(): PrimitivesOverviewOptions[] { + return ['name', 'all']; + } + + protected getAvailableOptions(): PrimitivesOverviewOptions[] { + return this.modules; + } + + protected getNewOption(): PrimitivesOverviewOptions { + return this.getAvailableOptions()[0]; + } + + protected getOptionName(option: PrimitivesOverviewOptions): string { + switch (option) { + case 'name': + return 'Primitive Name'; + case 'all': + return 'Count (Total)'; + default: + return option.name; + } + } + + protected getValue(item: string, option: PrimitivesOverviewOptions): DataGridCell { + switch (option) { + case 'name': + return '$' + item; + case 'all': + return this.modules[0].globalPrimitives.get(item)?.toString() ?? '-'; + default: { + const count = option.globalPrimitives.get(item); + const totalCount = this.modules[0].globalPrimitives.get(item); + if (count === undefined || !totalCount) { + return '-'; + } + + return `${count} (${getPercentage(count, totalCount)}%)`; + } + } + } + + update() { + this.reset(false, true); + + // Sort primitives by total occurence, descending + const allPrims = [...this.modules[0].globalPrimitives.entries()] + .sort((a, b) => b[1] - a[1]) + .map((entry) => entry[0]); + + for (const primitive of allPrims) { + this.addRowItem(primitive); + } + + this.render(); + } +} + interface Tab { title: string; element: CustomElement; diff --git a/views/digitaljs/src/viewers/stats/modules.ts b/views/digitaljs/src/viewers/stats/modules.ts index 07f6161..abe0eda 100644 --- a/views/digitaljs/src/viewers/stats/modules.ts +++ b/views/digitaljs/src/viewers/stats/modules.ts @@ -30,10 +30,12 @@ export const getModuleStatName = (stat: ModuleStatId): string => { export class Module { private parents: Set; - private _primitives: Map; public readonly name: string; + private _primitives: Map; + private _globalPrimitives: Map; + private _children: Map; private _globalChildren: Map; @@ -42,6 +44,13 @@ export class Module { constructor(name: string, stats: YosysModuleStats) { this.name = name; + + this._primitives = new Map(); + this._globalPrimitives = new Map(); + + this._children = new Map(); + this._globalChildren = new Map(); + this._stats = { memoryCount: stats.num_memories, memoryBitCount: stats.num_memory_bits, @@ -50,11 +59,6 @@ export class Module { }; this._globalStats = structuredClone(this._stats); - this._children = new Map(); - this._globalChildren = new Map(); - - this._primitives = new Map(); - this.parents = new Set(); } @@ -66,6 +70,10 @@ export class Module { return this._primitives; } + get globalPrimitives(): Map { + return this._globalPrimitives; + } + get children(): Map { return this._children; } @@ -84,6 +92,7 @@ export class Module { addPrimitive(prim: string, count: number) { this._primitives.set(prim, (this._primitives.get(prim) ?? 0) + count); + this._globalPrimitives.set(prim, (this._globalPrimitives.get(prim) ?? 0) + count); } addChild(module: Module, count: number) { @@ -100,6 +109,11 @@ export class Module { for (const [gloModule, gloCount] of module.globalChildren.entries()) { this._globalChildren.set(gloModule, (this._globalChildren.get(gloModule) ?? 0) + count * gloCount); } + + // Update global primitives + for (const [gloPrim, gloCount] of module.globalPrimitives.entries()) { + this._globalPrimitives.set(gloPrim, (this._globalPrimitives.get(gloPrim) ?? 0) + count * gloCount); + } } protected addParent(module: Module) { diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/stats.ts index 3e7c1ae..bd00c0d 100644 --- a/views/digitaljs/src/viewers/stats/stats.ts +++ b/views/digitaljs/src/viewers/stats/stats.ts @@ -3,7 +3,7 @@ import type {ForeignViewMessage} from '../../messages'; import type {YosysStats} from '../../types'; import {BaseViewer} from '../base'; -import {ModuleExplorerGrid, ModuleOverviewGrid, TabsContainer} from './elements'; +import {ModuleExplorerGrid, ModuleOverviewGrid, PrimitivesOverviewGrid, TabsContainer} from './elements'; import {type Module, type ModuleStatId, buildModuleTree} from './modules'; interface GridColumnSettings { @@ -33,6 +33,7 @@ export class StatsViewer extends BaseViewer { private moduleOverview: ModuleOverviewGrid; private moduleExplorer: ModuleExplorerGrid; + private primitivesOverview: PrimitivesOverviewGrid; constructor(mainView: View, initData: YosysStats) { super(mainView, initData); @@ -53,6 +54,8 @@ export class StatsViewer extends BaseViewer { } }); + this.primitivesOverview = new PrimitivesOverviewGrid(this.modules); + this.moduleExplorer = new ModuleExplorerGrid(this.modules[0]); this.moduleExplorer.addEventListener('gridHeadersUpdate', (data) => { this.storeValue('yosys-stats-explorer-settings', {columns: data.newHeaders}); @@ -66,7 +69,8 @@ export class StatsViewer extends BaseViewer { this.tabsContainer = new TabsContainer([ {title: 'Overview', element: this.moduleOverview}, - {title: 'Explorer', element: this.moduleExplorer} + {title: 'Explorer', element: this.moduleExplorer}, + {title: 'Primitives', element: this.primitivesOverview} ]); } From dce393762610240f956de58639df8e9a7e4719c7 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 14 Oct 2023 20:20:33 +0200 Subject: [PATCH 20/31] Add wire stats --- views/digitaljs/src/viewers/stats/modules.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/modules.ts b/views/digitaljs/src/viewers/stats/modules.ts index abe0eda..372b6cf 100644 --- a/views/digitaljs/src/viewers/stats/modules.ts +++ b/views/digitaljs/src/viewers/stats/modules.ts @@ -4,7 +4,9 @@ export enum ModuleStatId { memoryCount = 'memoryCount', memoryBitCount = 'memoryBitCount', processCount = 'processCount', - cellCount = 'cellCount' + cellCount = 'cellCount', + wireCount = 'wireCount', + wireCountPub = 'wireCountPub' } type ModuleStats = Record; @@ -23,8 +25,10 @@ export const getModuleStatName = (stat: ModuleStatId): string => { return 'Processes'; case ModuleStatId.cellCount: return 'Cells'; - default: - return ''; + case ModuleStatId.wireCount: + return 'Wires'; + case ModuleStatId.wireCountPub: + return 'Wires (Pub.)'; } }; @@ -55,7 +59,9 @@ export class Module { memoryCount: stats.num_memories, memoryBitCount: stats.num_memory_bits, processCount: stats.num_processes, - cellCount: stats.num_cells + cellCount: stats.num_cells, + wireCount: stats.num_wires, + wireCountPub: stats.num_pub_wires }; this._globalStats = structuredClone(this._stats); From 8491c3dda67bdcda688ff45c2aa08b3a6f6d684e Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 14 Oct 2023 20:27:22 +0200 Subject: [PATCH 21/31] Show default count col in overview tab --- views/digitaljs/src/viewers/stats/elements.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 167bb36..5ffc44f 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -352,7 +352,7 @@ export class ModuleOverviewGrid extends InteractiveDataGrid Date: Sat, 14 Oct 2023 20:39:34 +0200 Subject: [PATCH 22/31] Reverse rows and columns --- views/digitaljs/src/viewers/stats/elements.ts | 59 ++++++++----------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts index 5ffc44f..eebbd1f 100644 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ b/views/digitaljs/src/viewers/stats/elements.ts @@ -555,9 +555,10 @@ export class ModuleExplorerGrid extends InteractiveDataGrid { +export class PrimitivesOverviewGrid extends InteractiveDataGrid { private modules: Module[]; constructor(modules: Module[]) { @@ -567,62 +568,50 @@ export class PrimitivesOverviewGrid extends InteractiveDataGrid b[1] - a[1]) - .map((entry) => entry[0]); - - for (const primitive of allPrims) { - this.addRowItem(primitive); + for (const module of this.modules) { + this.addRowItem(module); } this.render(); From 76875845a765a70261b529f7b14d5f8039ca7fc6 Mon Sep 17 00:00:00 2001 From: Mike A Date: Sat, 14 Oct 2023 21:03:56 +0200 Subject: [PATCH 23/31] refactor stat viewer elements into separate files --- .../viewers/diagram/{diagram.ts => index.ts} | 0 views/digitaljs/src/viewers/index.ts | 6 +- views/digitaljs/src/viewers/stats/elements.ts | 663 ------------------ .../src/viewers/stats/elements/base.ts | 23 + .../src/viewers/stats/elements/datagrid.ts | 307 ++++++++ .../src/viewers/stats/elements/explorer.ts | 169 +++++ .../src/viewers/stats/elements/index.ts | 4 + .../viewers/stats/elements/moduleoverview.ts | 58 ++ .../src/viewers/stats/elements/primitives.ts | 67 ++ .../src/viewers/stats/elements/tabs.ts | 45 ++ .../src/viewers/stats/elements/util.ts | 7 + .../src/viewers/stats/{stats.ts => index.ts} | 0 12 files changed, 683 insertions(+), 666 deletions(-) rename views/digitaljs/src/viewers/diagram/{diagram.ts => index.ts} (100%) delete mode 100644 views/digitaljs/src/viewers/stats/elements.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/base.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/datagrid.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/explorer.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/index.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/moduleoverview.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/primitives.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/tabs.ts create mode 100644 views/digitaljs/src/viewers/stats/elements/util.ts rename views/digitaljs/src/viewers/stats/{stats.ts => index.ts} (100%) diff --git a/views/digitaljs/src/viewers/diagram/diagram.ts b/views/digitaljs/src/viewers/diagram/index.ts similarity index 100% rename from views/digitaljs/src/viewers/diagram/diagram.ts rename to views/digitaljs/src/viewers/diagram/index.ts diff --git a/views/digitaljs/src/viewers/index.ts b/views/digitaljs/src/viewers/index.ts index 79cf11f..1e17c1d 100644 --- a/views/digitaljs/src/viewers/index.ts +++ b/views/digitaljs/src/viewers/index.ts @@ -1,3 +1,3 @@ -export * from './diagram/diagram'; -export * from './stats/stats'; -export * from './base'; +export {DiagramViewer} from './diagram'; +export {StatsViewer} from './stats'; +export {BaseViewer} from './base'; diff --git a/views/digitaljs/src/viewers/stats/elements.ts b/views/digitaljs/src/viewers/stats/elements.ts deleted file mode 100644 index eebbd1f..0000000 --- a/views/digitaljs/src/viewers/stats/elements.ts +++ /dev/null @@ -1,663 +0,0 @@ -import {type Module, type ModuleStatId, getModuleStatIds, getModuleStatName} from './modules'; - -const getPercentage = (val1: number, val2: number): number => { - if (val1 === 0 || val2 === 0) { - return 0; - } - - return Math.floor((val1 / val2) * 10_000) / 100; -}; - -export abstract class CustomElement { - protected abstract rootElem: HTMLElement; - - private eventsElem: Element; - - constructor() { - this.eventsElem = document.createElement('events'); - } - - addEventListener(type: K, listener: (ev: EventsDirectory[K]) => void) { - this.eventsElem.addEventListener(type, (ev: CustomEventInit) => listener(ev.detail)); - } - - protected dispatchEvent(type: K, data: EventsDirectory[K]) { - this.eventsElem.dispatchEvent(new CustomEvent(type, {detail: data})); - } - - get element() { - return this.rootElem; - } - - abstract render(): void; -} - -type DataGridCell = Node | string; - -export class DataGrid extends CustomElement { - protected rootElem: HTMLElement; - - protected cells: DataGridCell[][]; - - constructor(headers: string[] = []) { - super(); - - this.cells = [headers]; - this.rootElem = document.createElement('vscode-data-grid'); - } - - get width() { - return this.cells[0].length; - } - - get height() { - return this.cells.length; - } - - addColumn(contents: DataGridCell[], pos?: number): number { - contents = contents.slice(0, this.height); - if (contents.length < this.height) { - contents = contents.concat(new Array(this.height - contents.length)); - } - - if (pos === undefined) { - pos = this.width; - } - - for (let i = 0; i < this.height; i++) { - this.cells[i].splice(pos, 0, contents[i]); - } - - return pos; - } - - delColumn(pos: number) { - for (const row of this.cells) { - row.splice(pos, 1); - } - } - - addRow(contents: DataGridCell[], pos?: number): number { - contents = contents.slice(0, this.width); - if (contents.length < this.width) { - contents = contents.concat(new Array(this.height - contents.length)); - } - - if (pos === undefined) { - pos = this.height - 1; - } - - this.cells.splice(pos + 1, 0, contents); - - return pos; - } - - setCell(x: number, y: number, value: DataGridCell) { - this.cells[y][x] = value; - - const row = this.rootElem.querySelectorAll('vscode-data-grid-row')[y]; - if (!row) { - return; - } - const cell = row.querySelectorAll('vscode-data-grid-cell')[x]; - if (!cell) { - return; - } - cell.replaceChildren(); - cell.append(value); - } - - clearRows() { - this.cells.splice(1); - } - - render() { - const newRoot = document.createElement('vscode-data-grid'); - this.rootElem.replaceWith(newRoot); - this.rootElem = newRoot; - - // Headers - const headerElem = document.createElement('vscode-data-grid-row'); - headerElem.setAttribute('row-type', 'header'); - - for (let i = 0; i < this.width; i++) { - const cell = document.createElement('vscode-data-grid-cell'); - cell.setAttribute('cell-type', 'columnheader'); - cell.setAttribute('grid-column', (i + 1).toString()); - cell.append(this.cells[0][i]); - headerElem.appendChild(cell); - } - this.rootElem.appendChild(headerElem); - - // Rows - for (let rowi = 1; rowi < this.height; rowi++) { - const row = document.createElement('vscode-data-grid-row'); - - for (let coli = 0; coli < this.width; coli++) { - const cell = document.createElement('vscode-data-grid-cell'); - cell.setAttribute('grid-column', (coli + 1).toString()); - cell.append(this.cells[rowi][coli]); - row.appendChild(cell); - } - this.rootElem.appendChild(row); - } - } -} - -interface GridHeadersUpdateEvent { - newHeaders: ColumnOption[]; -} -interface InteractiveDataGridEvents { - gridHeadersUpdate: GridHeadersUpdateEvent; -} - -abstract class InteractiveDataGrid extends DataGrid> { - private rows: RowItem[]; - private cols: ColumnOption[]; - - private actualRoot: HTMLElement; - - constructor() { - super(); - - this.rows = []; - this.cols = []; - - // The object renders to this.rootElem, but we want buttons outside - // the datagrid. This actualRoot is just a div containing those buttons - // and the rendered rootElem so we can 'adopt' the buttons into this class. - this.actualRoot = this.createRoot(); - - this.reset(true, true); - } - - override get element(): HTMLElement { - return this.actualRoot; - } - - protected abstract getDefaultOptions(): ColumnOption[]; - - protected abstract getAvailableOptions(): ColumnOption[]; - - protected abstract getNewOption(): ColumnOption; - - protected abstract getOptionName(option: ColumnOption): string; - - protected abstract getValue(item: RowItem, option: ColumnOption): DataGridCell; - - private createRoot() { - const root = document.createElement('div'); - - // Add column button - const addColBtn = document.createElement('vscode-button'); - addColBtn.innerHTML = /* html */ ``; - addColBtn.addEventListener('click', (_ev) => { - this.addCol(); - - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); - }); - root.appendChild(addColBtn); - - // Reset button - const resetBtn = document.createElement('vscode-button'); - resetBtn.innerHTML = /* html */ ``; - resetBtn.addEventListener('click', (_ev) => { - this.reset(true, false); - - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); - }); - root.appendChild(resetBtn); - - // The actual DataGrid - root.appendChild(super.element); - - return root; - } - - protected fillCell(x: number, y: number) { - const defColsLen = this.getDefaultOptions().length; - if (x < 0 || x >= defColsLen + this.cols.length || y < 1 || y - 1 >= this.rows.length) { - throw new Error('Out of bounds!'); - } - - const option = this.getDefaultOptions().concat(this.cols)[x]; - const item = this.rows[y - 1]; - const value = this.getValue(item, option); - - return super.setCell(x, y, value); - } - - addRowItem(item: RowItem): number { - this.rows.push(item); - const y = super.addRow([]); - - if (y !== 0) { - for (let x = 0; x < this.width; x++) { - this.fillCell(x, y); - } - } - - return y; - } - - addCol(option?: ColumnOption, onlyDraw = false): number { - if (option === undefined) { - option = this.getNewOption(); - } - if (!onlyDraw) { - this.cols.push(option); - } - - const header = document.createElement('div'); - - const delBtn = document.createElement('vscode-button'); - delBtn.innerHTML = /* html */ ``; - delBtn.addEventListener('click', (_ev) => { - const coli = this.cells[0].indexOf(header); - const defColCount = this.getDefaultOptions().length; - if (coli >= defColCount) { - this.cols.splice(coli - defColCount, 1); - this.delColumn(coli); - this.render(); - - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); - } - }); - header.appendChild(delBtn); - - const dropdown = document.createElement('vscode-dropdown') as HTMLSelectElement; - for (const avOption of this.getAvailableOptions()) { - const opt = document.createElement('vscode-option'); - opt.textContent = this.getOptionName(avOption); - if (avOption === option) { - opt.setAttribute('selected', ''); - } - dropdown.appendChild(opt); - } - header.appendChild(dropdown); - - dropdown.addEventListener('change', (_ev) => { - const headerIndex = this.cells[0].indexOf(header); - if (headerIndex === -1) return; - - const newOption = this.getAvailableOptions()[dropdown.selectedIndex]; - - // Actually update the column and re-render - this.cols.splice(headerIndex - this.getDefaultOptions().length, 1, newOption); - this.render(); - - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); - }); - - const pos = super.addColumn([header]); - - this.render(); - - for (let y = 1; y < this.height; y++) { - this.fillCell(pos, y); - } - - return pos; - } - - reset(resetCols: boolean, resetRows: boolean) { - if (resetCols) this.cols = []; - if (resetRows) this.rows = []; - - // Clear all row/cols - this.clearRows(); - while (this.width > 0) { - this.delColumn(0); - } - - // Restore them again - for (const option of this.getDefaultOptions()) { - super.addColumn([this.getOptionName(option)]); - } - for (const col of this.cols) { - this.addCol(col, true); - } - - this.render(); - } - - render() { - this.clearRows(); - - this.rows.forEach(() => super.addRow([])); - - for (let y = 1; y < this.height; y++) { - for (let x = 0; x < this.width; x++) { - this.fillCell(x, y); - } - } - - super.render(); - } -} - -type ModuleOverviewOptions = 'name' | 'count' | ModuleStatId; - -export class ModuleOverviewGrid extends InteractiveDataGrid { - private modules: Module[]; - - constructor(modules: Module[]) { - super(); - - this.modules = modules; - - for (const module of modules) { - this.addRowItem(module); - } - } - - protected getDefaultOptions(): ModuleOverviewOptions[] { - return ['name', 'count']; - } - - protected getAvailableOptions(): ModuleOverviewOptions[] { - return getModuleStatIds(); - } - - protected getNewOption(): ModuleOverviewOptions { - return this.getAvailableOptions()[0]; - } - - protected getOptionName(option: ModuleOverviewOptions): string { - switch (option) { - case 'name': - return 'Module name'; - case 'count': - return 'Count'; - default: - return getModuleStatName(option); - } - } - - protected getValue(item: Module, option: ModuleOverviewOptions): DataGridCell { - switch (option) { - case 'name': - return item.name; - case 'count': - return this.modules[0].globalChildren.get(item)?.toString() ?? '-'; - default: { - const stat1 = item.globalStats[option]; - const stat2 = this.modules[0].globalStats[option]; - - return `${stat1}/${stat2} (${getPercentage(stat1, stat2)}%)`; - } - } - } -} - -interface ModuleExplorerRowCurrent { - type: 'current'; -} -interface ModuleExplorerRowPrimitive { - type: 'primitive'; - primitive: string; -} -interface ModuleExplorerRowChild { - type: 'child'; - module: Module; -} -type ModuleExplorerRowItems = ModuleExplorerRowCurrent | ModuleExplorerRowPrimitive | ModuleExplorerRowChild; -type ModuleExplorerOptions = 'name' | 'count' | ModuleStatId; - -export class ModuleExplorerGrid extends InteractiveDataGrid { - private actualRoot2: HTMLElement; - private breadcrumbHeader: HTMLParagraphElement; - - private moduleBreadcrumbs: Module[]; - - constructor(initModule: Module) { - super(); - - this.breadcrumbHeader = document.createElement('p'); - this.actualRoot2 = document.createElement('div'); - this.actualRoot2.appendChild(this.breadcrumbHeader); - this.actualRoot2.appendChild(super.element); - - this.moduleBreadcrumbs = [initModule]; - - this.update(); - } - - override get element(): HTMLElement { - return this.actualRoot2; - } - - get curModule(): Module { - return this.moduleBreadcrumbs[this.moduleBreadcrumbs.length - 1]; - } - - navigateSplice(i: number) { - if (i === 0) { - throw new Error('Cannot navigate above top-level module!'); - } - this.moduleBreadcrumbs.splice(i); - } - - navigate(module: Module) { - if (Array.from(this.curModule.children.keys()).indexOf(module) === -1) { - throw new Error(`Cannot navigate to module "${module.name}": not a child of "${this.curModule.name}"`); - } - this.moduleBreadcrumbs.push(module); - } - - update() { - // Update breadcrumbs - this.breadcrumbHeader.replaceChildren(); - - for (let i = 0; i < this.moduleBreadcrumbs.length; i++) { - const link = document.createElement('vscode-link'); - link.textContent = this.moduleBreadcrumbs[i].name; - link.addEventListener('click', (_ev) => { - this.navigateSplice(i + 1); - this.update(); - }); - this.breadcrumbHeader.append(link); - - if (i != this.moduleBreadcrumbs.length - 1) { - this.breadcrumbHeader.append(' > '); - } - } - - // Update grid - this.reset(false, true); - this.addRowItem({type: 'current'}); - for (const prim of this.curModule.primitives.keys()) { - this.addRowItem({type: 'primitive', primitive: prim}); - } - for (const subCircuit of this.curModule.children.keys()) { - this.addRowItem({type: 'child', module: subCircuit}); - } - - this.render(); - } - - protected getDefaultOptions(): ModuleExplorerOptions[] { - return ['name', 'count']; - } - - protected getAvailableOptions(): ModuleExplorerOptions[] { - return getModuleStatIds(); - } - - protected getNewOption(): ModuleExplorerOptions { - return this.getAvailableOptions()[0]; - } - - protected getOptionName(option: ModuleOverviewOptions): string { - switch (option) { - case 'name': - return 'Module name'; - case 'count': - return 'Count'; - default: - return getModuleStatName(option); - } - } - - protected getValue(item: ModuleExplorerRowItems, option: ModuleOverviewOptions): DataGridCell { - switch (item.type) { - case 'current': - return this.getValueCurrent(item, option); - case 'primitive': - return this.getValuePrimitive(item, option); - case 'child': - return this.getValueChild(item, option); - } - } - - private getValueCurrent(_item: ModuleExplorerRowCurrent, option: ModuleOverviewOptions): DataGridCell { - switch (option) { - case 'name': - return '< Current Module >'; - case 'count': - return '-'; - default: - return this.curModule.globalStats[option].toString(); - } - } - - private getValuePrimitive(item: ModuleExplorerRowPrimitive, option: ModuleOverviewOptions): DataGridCell { - switch (option) { - case 'name': - return item.primitive; - case 'count': - return this.curModule.primitives.get(item.primitive)?.toString() ?? '-'; - default: - return ''; - } - } - - private getValueChild(item: ModuleExplorerRowChild, option: ModuleOverviewOptions): DataGridCell { - switch (option) { - case 'name': { - const link = document.createElement('vscode-link'); - link.textContent = item.module.name; - link.addEventListener('click', (_ev) => { - this.navigate(item.module); - this.update(); - }); - return link; - } - case 'count': - return this.curModule.children.get(item.module)?.toString() ?? '-'; - default: { - const stat1 = item.module.globalStats[option]; - const stat2 = this.curModule.globalStats[option]; - - return `${stat1} (${getPercentage(stat1, stat2)}%)`; - } - } - } -} - -// TODO: typing for primitives (needs exhaustive list) -type PrimitivesOverviewOptions = 'name' | string; - -export class PrimitivesOverviewGrid extends InteractiveDataGrid { - private modules: Module[]; - - constructor(modules: Module[]) { - super(); - - this.modules = modules; - - this.update(); - - const prims = this.getAvailableOptions(); - for (let i = 0; i < prims.length && i < 5; i++) { - this.addCol(prims[i]); - } - } - - protected getDefaultOptions(): PrimitivesOverviewOptions[] { - return ['name']; - } - - protected getAvailableOptions(): PrimitivesOverviewOptions[] { - return Array.from(this.modules[0].globalPrimitives.keys()); - } - - protected getNewOption(): PrimitivesOverviewOptions { - return this.getAvailableOptions()[0]; - } - - protected getOptionName(option: string): string { - if (option === 'name') { - return 'Module name'; - } - return '$' + option; - } - - protected getValue(item: Module, option: PrimitivesOverviewOptions): DataGridCell { - if (option === 'name') { - return item.name; - } - - const count = item.globalPrimitives.get(option); - const totalCount = this.modules[0].globalPrimitives.get(option); - if (count === undefined || !totalCount) { - return '-'; - } - - return `${count} (${getPercentage(count, totalCount)}%)`; - } - - update() { - this.reset(false, true); - - for (const module of this.modules) { - this.addRowItem(module); - } - - this.render(); - } -} - -interface Tab { - title: string; - element: CustomElement; -} - -export class TabsContainer extends CustomElement> { - protected rootElem: HTMLElement; - - private tabs: Tab[]; - - constructor(tabs: Tab[]) { - super(); - - this.tabs = tabs; - - this.rootElem = this.createRoot(); - } - - private createRoot(): HTMLElement { - const root = document.createElement('vscode-panels'); - - for (let i = 0; i < this.tabs.length; i++) { - const tab = document.createElement('vscode-panel-tab'); - tab.id = `tab-${i}`; - tab.textContent = this.tabs[i].title; - root.appendChild(tab); - } - for (let i = 0; i < this.tabs.length; i++) { - const view = document.createElement('vscode-panel-view'); - view.id = `view-${i}`; - view.appendChild(this.tabs[i].element.element); - root.appendChild(view); - } - - return root; - } - - render(): void { - for (const tab of this.tabs) { - tab.element.render(); - } - } -} diff --git a/views/digitaljs/src/viewers/stats/elements/base.ts b/views/digitaljs/src/viewers/stats/elements/base.ts new file mode 100644 index 0000000..6abe5dc --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/base.ts @@ -0,0 +1,23 @@ +export abstract class CustomElement { + protected abstract rootElem: HTMLElement; + + private eventsElem: Element; + + constructor() { + this.eventsElem = document.createElement('events'); + } + + addEventListener(type: K, listener: (ev: EventsDirectory[K]) => void) { + this.eventsElem.addEventListener(type, (ev: CustomEventInit) => listener(ev.detail)); + } + + protected dispatchEvent(type: K, data: EventsDirectory[K]) { + this.eventsElem.dispatchEvent(new CustomEvent(type, {detail: data})); + } + + get element() { + return this.rootElem; + } + + abstract render(): void; +} diff --git a/views/digitaljs/src/viewers/stats/elements/datagrid.ts b/views/digitaljs/src/viewers/stats/elements/datagrid.ts new file mode 100644 index 0000000..4be8f67 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/datagrid.ts @@ -0,0 +1,307 @@ +import {CustomElement} from './base'; + +export type DataGridCell = Node | string; + +class DataGrid extends CustomElement { + protected rootElem: HTMLElement; + + protected cells: DataGridCell[][]; + + constructor(headers: string[] = []) { + super(); + + this.cells = [headers]; + this.rootElem = document.createElement('vscode-data-grid'); + } + + get width() { + return this.cells[0].length; + } + + get height() { + return this.cells.length; + } + + addColumn(contents: DataGridCell[], pos?: number): number { + contents = contents.slice(0, this.height); + if (contents.length < this.height) { + contents = contents.concat(new Array(this.height - contents.length)); + } + + if (pos === undefined) { + pos = this.width; + } + + for (let i = 0; i < this.height; i++) { + this.cells[i].splice(pos, 0, contents[i]); + } + + return pos; + } + + delColumn(pos: number) { + for (const row of this.cells) { + row.splice(pos, 1); + } + } + + addRow(contents: DataGridCell[], pos?: number): number { + contents = contents.slice(0, this.width); + if (contents.length < this.width) { + contents = contents.concat(new Array(this.height - contents.length)); + } + + if (pos === undefined) { + pos = this.height - 1; + } + + this.cells.splice(pos + 1, 0, contents); + + return pos; + } + + setCell(x: number, y: number, value: DataGridCell) { + this.cells[y][x] = value; + + const row = this.rootElem.querySelectorAll('vscode-data-grid-row')[y]; + if (!row) { + return; + } + const cell = row.querySelectorAll('vscode-data-grid-cell')[x]; + if (!cell) { + return; + } + cell.replaceChildren(); + cell.append(value); + } + + clearRows() { + this.cells.splice(1); + } + + render() { + const newRoot = document.createElement('vscode-data-grid'); + this.rootElem.replaceWith(newRoot); + this.rootElem = newRoot; + + // Headers + const headerElem = document.createElement('vscode-data-grid-row'); + headerElem.setAttribute('row-type', 'header'); + + for (let i = 0; i < this.width; i++) { + const cell = document.createElement('vscode-data-grid-cell'); + cell.setAttribute('cell-type', 'columnheader'); + cell.setAttribute('grid-column', (i + 1).toString()); + cell.append(this.cells[0][i]); + headerElem.appendChild(cell); + } + this.rootElem.appendChild(headerElem); + + // Rows + for (let rowi = 1; rowi < this.height; rowi++) { + const row = document.createElement('vscode-data-grid-row'); + + for (let coli = 0; coli < this.width; coli++) { + const cell = document.createElement('vscode-data-grid-cell'); + cell.setAttribute('grid-column', (coli + 1).toString()); + cell.append(this.cells[rowi][coli]); + row.appendChild(cell); + } + this.rootElem.appendChild(row); + } + } +} + +interface GridHeadersUpdateEvent { + newHeaders: ColumnOption[]; +} +interface InteractiveDataGridEvents { + gridHeadersUpdate: GridHeadersUpdateEvent; +} + +export abstract class InteractiveDataGrid extends DataGrid< + InteractiveDataGridEvents +> { + private rows: RowItem[]; + private cols: ColumnOption[]; + + private actualRoot: HTMLElement; + + constructor() { + super(); + + this.rows = []; + this.cols = []; + + // The object renders to this.rootElem, but we want buttons outside + // the datagrid. This actualRoot is just a div containing those buttons + // and the rendered rootElem so we can 'adopt' the buttons into this class. + this.actualRoot = this.createRoot(); + + this.reset(true, true); + } + + override get element(): HTMLElement { + return this.actualRoot; + } + + protected abstract getDefaultOptions(): ColumnOption[]; + + protected abstract getAvailableOptions(): ColumnOption[]; + + protected abstract getNewOption(): ColumnOption; + + protected abstract getOptionName(option: ColumnOption): string; + + protected abstract getValue(item: RowItem, option: ColumnOption): DataGridCell; + + private createRoot() { + const root = document.createElement('div'); + + // Add column button + const addColBtn = document.createElement('vscode-button'); + addColBtn.innerHTML = /* html */ ``; + addColBtn.addEventListener('click', (_ev) => { + this.addCol(); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + }); + root.appendChild(addColBtn); + + // Reset button + const resetBtn = document.createElement('vscode-button'); + resetBtn.innerHTML = /* html */ ``; + resetBtn.addEventListener('click', (_ev) => { + this.reset(true, false); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + }); + root.appendChild(resetBtn); + + // The actual DataGrid + root.appendChild(super.element); + + return root; + } + + protected fillCell(x: number, y: number) { + const defColsLen = this.getDefaultOptions().length; + if (x < 0 || x >= defColsLen + this.cols.length || y < 1 || y - 1 >= this.rows.length) { + throw new Error('Out of bounds!'); + } + + const option = this.getDefaultOptions().concat(this.cols)[x]; + const item = this.rows[y - 1]; + const value = this.getValue(item, option); + + return super.setCell(x, y, value); + } + + addRowItem(item: RowItem): number { + this.rows.push(item); + const y = super.addRow([]); + + if (y !== 0) { + for (let x = 0; x < this.width; x++) { + this.fillCell(x, y); + } + } + + return y; + } + + addCol(option?: ColumnOption, onlyDraw = false): number { + if (option === undefined) { + option = this.getNewOption(); + } + if (!onlyDraw) { + this.cols.push(option); + } + + const header = document.createElement('div'); + + const delBtn = document.createElement('vscode-button'); + delBtn.innerHTML = /* html */ ``; + delBtn.addEventListener('click', (_ev) => { + const coli = this.cells[0].indexOf(header); + const defColCount = this.getDefaultOptions().length; + if (coli >= defColCount) { + this.cols.splice(coli - defColCount, 1); + this.delColumn(coli); + this.render(); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + } + }); + header.appendChild(delBtn); + + const dropdown = document.createElement('vscode-dropdown') as HTMLSelectElement; + for (const avOption of this.getAvailableOptions()) { + const opt = document.createElement('vscode-option'); + opt.textContent = this.getOptionName(avOption); + if (avOption === option) { + opt.setAttribute('selected', ''); + } + dropdown.appendChild(opt); + } + header.appendChild(dropdown); + + dropdown.addEventListener('change', (_ev) => { + const headerIndex = this.cells[0].indexOf(header); + if (headerIndex === -1) return; + + const newOption = this.getAvailableOptions()[dropdown.selectedIndex]; + + // Actually update the column and re-render + this.cols.splice(headerIndex - this.getDefaultOptions().length, 1, newOption); + this.render(); + + this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + }); + + const pos = super.addColumn([header]); + + this.render(); + + for (let y = 1; y < this.height; y++) { + this.fillCell(pos, y); + } + + return pos; + } + + reset(resetCols: boolean, resetRows: boolean) { + if (resetCols) this.cols = []; + if (resetRows) this.rows = []; + + // Clear all row/cols + this.clearRows(); + while (this.width > 0) { + this.delColumn(0); + } + + // Restore them again + for (const option of this.getDefaultOptions()) { + super.addColumn([this.getOptionName(option)]); + } + for (const col of this.cols) { + this.addCol(col, true); + } + + this.render(); + } + + render() { + this.clearRows(); + + this.rows.forEach(() => super.addRow([])); + + for (let y = 1; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + this.fillCell(x, y); + } + } + + super.render(); + } +} diff --git a/views/digitaljs/src/viewers/stats/elements/explorer.ts b/views/digitaljs/src/viewers/stats/elements/explorer.ts new file mode 100644 index 0000000..6bb2087 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/explorer.ts @@ -0,0 +1,169 @@ +import {type Module, type ModuleStatId, getModuleStatIds, getModuleStatName} from '../modules'; + +import {type DataGridCell, InteractiveDataGrid} from './datagrid'; +import {getPercentage} from './util'; + +interface ModuleExplorerRowCurrent { + type: 'current'; +} +interface ModuleExplorerRowPrimitive { + type: 'primitive'; + primitive: string; +} +interface ModuleExplorerRowChild { + type: 'child'; + module: Module; +} +type ModuleExplorerRowItems = ModuleExplorerRowCurrent | ModuleExplorerRowPrimitive | ModuleExplorerRowChild; +type ModuleExplorerOptions = 'name' | 'count' | ModuleStatId; + +export class ModuleExplorerGrid extends InteractiveDataGrid { + private actualRoot2: HTMLElement; + private breadcrumbHeader: HTMLParagraphElement; + + private moduleBreadcrumbs: Module[]; + + constructor(initModule: Module) { + super(); + + this.breadcrumbHeader = document.createElement('p'); + this.actualRoot2 = document.createElement('div'); + this.actualRoot2.appendChild(this.breadcrumbHeader); + this.actualRoot2.appendChild(super.element); + + this.moduleBreadcrumbs = [initModule]; + + this.update(); + } + + override get element(): HTMLElement { + return this.actualRoot2; + } + + get curModule(): Module { + return this.moduleBreadcrumbs[this.moduleBreadcrumbs.length - 1]; + } + + navigateSplice(i: number) { + if (i === 0) { + throw new Error('Cannot navigate above top-level module!'); + } + this.moduleBreadcrumbs.splice(i); + } + + navigate(module: Module) { + if (Array.from(this.curModule.children.keys()).indexOf(module) === -1) { + throw new Error(`Cannot navigate to module "${module.name}": not a child of "${this.curModule.name}"`); + } + this.moduleBreadcrumbs.push(module); + } + + update() { + // Update breadcrumbs + this.breadcrumbHeader.replaceChildren(); + + for (let i = 0; i < this.moduleBreadcrumbs.length; i++) { + const link = document.createElement('vscode-link'); + link.textContent = this.moduleBreadcrumbs[i].name; + link.addEventListener('click', (_ev) => { + this.navigateSplice(i + 1); + this.update(); + }); + this.breadcrumbHeader.append(link); + + if (i != this.moduleBreadcrumbs.length - 1) { + this.breadcrumbHeader.append(' > '); + } + } + + // Update grid + this.reset(false, true); + this.addRowItem({type: 'current'}); + for (const prim of this.curModule.primitives.keys()) { + this.addRowItem({type: 'primitive', primitive: prim}); + } + for (const subCircuit of this.curModule.children.keys()) { + this.addRowItem({type: 'child', module: subCircuit}); + } + + this.render(); + } + + protected getDefaultOptions(): ModuleExplorerOptions[] { + return ['name', 'count']; + } + + protected getAvailableOptions(): ModuleExplorerOptions[] { + return getModuleStatIds(); + } + + protected getNewOption(): ModuleExplorerOptions { + return this.getAvailableOptions()[0]; + } + + protected getOptionName(option: ModuleExplorerOptions): string { + switch (option) { + case 'name': + return 'Module name'; + case 'count': + return 'Count'; + default: + return getModuleStatName(option); + } + } + + protected getValue(item: ModuleExplorerRowItems, option: ModuleExplorerOptions): DataGridCell { + switch (item.type) { + case 'current': + return this.getValueCurrent(item, option); + case 'primitive': + return this.getValuePrimitive(item, option); + case 'child': + return this.getValueChild(item, option); + } + } + + private getValueCurrent(_item: ModuleExplorerRowCurrent, option: ModuleExplorerOptions): DataGridCell { + switch (option) { + case 'name': + return '< Current Module >'; + case 'count': + return '-'; + default: + return this.curModule.globalStats[option].toString(); + } + } + + private getValuePrimitive(item: ModuleExplorerRowPrimitive, option: ModuleExplorerOptions): DataGridCell { + switch (option) { + case 'name': + return item.primitive; + case 'count': + return this.curModule.primitives.get(item.primitive)?.toString() ?? '-'; + default: + return ''; + } + } + + private getValueChild(item: ModuleExplorerRowChild, option: ModuleExplorerOptions): DataGridCell { + switch (option) { + case 'name': { + const link = document.createElement('vscode-link'); + link.textContent = item.module.name; + link.addEventListener('click', (_ev) => { + this.navigate(item.module); + this.update(); + }); + return link; + } + case 'count': + return this.curModule.children.get(item.module)?.toString() ?? '-'; + default: { + const stat1 = item.module.globalStats[option]; + const stat2 = this.curModule.globalStats[option]; + + return `${stat1} (${getPercentage(stat1, stat2)}%)`; + } + } + } +} diff --git a/views/digitaljs/src/viewers/stats/elements/index.ts b/views/digitaljs/src/viewers/stats/elements/index.ts new file mode 100644 index 0000000..15ae558 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/index.ts @@ -0,0 +1,4 @@ +export {ModuleOverviewGrid} from './moduleoverview'; +export {TabsContainer} from './tabs'; +export {ModuleExplorerGrid} from './explorer'; +export {PrimitivesOverviewGrid} from './primitives'; diff --git a/views/digitaljs/src/viewers/stats/elements/moduleoverview.ts b/views/digitaljs/src/viewers/stats/elements/moduleoverview.ts new file mode 100644 index 0000000..dd00393 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/moduleoverview.ts @@ -0,0 +1,58 @@ +import {type Module, type ModuleStatId, getModuleStatIds, getModuleStatName} from '../modules'; + +import {type DataGridCell, InteractiveDataGrid} from './datagrid'; +import {getPercentage} from './util'; + +type ModuleOverviewOptions = 'name' | 'count' | ModuleStatId; + +export class ModuleOverviewGrid extends InteractiveDataGrid { + private modules: Module[]; + + constructor(modules: Module[]) { + super(); + + this.modules = modules; + + for (const module of modules) { + this.addRowItem(module); + } + } + + protected getDefaultOptions(): ModuleOverviewOptions[] { + return ['name', 'count']; + } + + protected getAvailableOptions(): ModuleOverviewOptions[] { + return getModuleStatIds(); + } + + protected getNewOption(): ModuleOverviewOptions { + return this.getAvailableOptions()[0]; + } + + protected getOptionName(option: ModuleOverviewOptions): string { + switch (option) { + case 'name': + return 'Module name'; + case 'count': + return 'Count'; + default: + return getModuleStatName(option); + } + } + + protected getValue(item: Module, option: ModuleOverviewOptions): DataGridCell { + switch (option) { + case 'name': + return item.name; + case 'count': + return this.modules[0].globalChildren.get(item)?.toString() ?? '-'; + default: { + const stat1 = item.globalStats[option]; + const stat2 = this.modules[0].globalStats[option]; + + return `${stat1}/${stat2} (${getPercentage(stat1, stat2)}%)`; + } + } + } +} diff --git a/views/digitaljs/src/viewers/stats/elements/primitives.ts b/views/digitaljs/src/viewers/stats/elements/primitives.ts new file mode 100644 index 0000000..8e025fc --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/primitives.ts @@ -0,0 +1,67 @@ +import {type Module} from '../modules'; + +import {type DataGridCell, InteractiveDataGrid} from './datagrid'; +import {getPercentage} from './util'; + +// TODO: typing for primitives (needs exhaustive list) +type PrimitivesOverviewOptions = 'name' | string; + +export class PrimitivesOverviewGrid extends InteractiveDataGrid { + private modules: Module[]; + + constructor(modules: Module[]) { + super(); + + this.modules = modules; + + this.update(); + + const prims = this.getAvailableOptions(); + for (let i = 0; i < prims.length && i < 5; i++) { + this.addCol(prims[i]); + } + } + + protected getDefaultOptions(): PrimitivesOverviewOptions[] { + return ['name']; + } + + protected getAvailableOptions(): PrimitivesOverviewOptions[] { + return Array.from(this.modules[0].globalPrimitives.keys()); + } + + protected getNewOption(): PrimitivesOverviewOptions { + return this.getAvailableOptions()[0]; + } + + protected getOptionName(option: string): string { + if (option === 'name') { + return 'Module name'; + } + return '$' + option; + } + + protected getValue(item: Module, option: PrimitivesOverviewOptions): DataGridCell { + if (option === 'name') { + return item.name; + } + + const count = item.globalPrimitives.get(option); + const totalCount = this.modules[0].globalPrimitives.get(option); + if (count === undefined || !totalCount) { + return '-'; + } + + return `${count} (${getPercentage(count, totalCount)}%)`; + } + + update() { + this.reset(false, true); + + for (const module of this.modules) { + this.addRowItem(module); + } + + this.render(); + } +} diff --git a/views/digitaljs/src/viewers/stats/elements/tabs.ts b/views/digitaljs/src/viewers/stats/elements/tabs.ts new file mode 100644 index 0000000..aaa8c91 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/tabs.ts @@ -0,0 +1,45 @@ +import {CustomElement} from './base'; + +interface Tab { + title: string; + element: CustomElement; +} + +export class TabsContainer extends CustomElement> { + protected rootElem: HTMLElement; + + private tabs: Tab[]; + + constructor(tabs: Tab[]) { + super(); + + this.tabs = tabs; + + this.rootElem = this.createRoot(); + } + + private createRoot(): HTMLElement { + const root = document.createElement('vscode-panels'); + + for (let i = 0; i < this.tabs.length; i++) { + const tab = document.createElement('vscode-panel-tab'); + tab.id = `tab-${i}`; + tab.textContent = this.tabs[i].title; + root.appendChild(tab); + } + for (let i = 0; i < this.tabs.length; i++) { + const view = document.createElement('vscode-panel-view'); + view.id = `view-${i}`; + view.appendChild(this.tabs[i].element.element); + root.appendChild(view); + } + + return root; + } + + render(): void { + for (const tab of this.tabs) { + tab.element.render(); + } + } +} diff --git a/views/digitaljs/src/viewers/stats/elements/util.ts b/views/digitaljs/src/viewers/stats/elements/util.ts new file mode 100644 index 0000000..fa593d4 --- /dev/null +++ b/views/digitaljs/src/viewers/stats/elements/util.ts @@ -0,0 +1,7 @@ +export const getPercentage = (val1: number, val2: number): number => { + if (val1 === 0 || val2 === 0) { + return 0; + } + + return Math.floor((val1 / val2) * 10_000) / 100; +}; diff --git a/views/digitaljs/src/viewers/stats/stats.ts b/views/digitaljs/src/viewers/stats/index.ts similarity index 100% rename from views/digitaljs/src/viewers/stats/stats.ts rename to views/digitaljs/src/viewers/stats/index.ts From 20f565bd2974caeb09832c6891714f9f86a24e1a Mon Sep 17 00:00:00 2001 From: Mike A Date: Sun, 15 Oct 2023 16:00:09 +0200 Subject: [PATCH 24/31] Add initial interactive datagrid settings support --- .../src/viewers/stats/elements/datagrid.ts | 43 +++++++++++++++++++ .../src/viewers/stats/elements/explorer.ts | 6 ++- .../viewers/stats/elements/moduleoverview.ts | 6 ++- .../src/viewers/stats/elements/primitives.ts | 14 ++++-- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements/datagrid.ts b/views/digitaljs/src/viewers/stats/elements/datagrid.ts index 4be8f67..cdf2cc0 100644 --- a/views/digitaljs/src/viewers/stats/elements/datagrid.ts +++ b/views/digitaljs/src/viewers/stats/elements/datagrid.ts @@ -112,6 +112,12 @@ class DataGrid extends CustomElement { } } +export interface DatagridSetting { + id: string; + text: string; + default: boolean; +} + interface GridHeadersUpdateEvent { newHeaders: ColumnOption[]; } @@ -127,12 +133,19 @@ export abstract class InteractiveDataGrid extends DataGri private actualRoot: HTMLElement; + private settingValues: Record; + constructor() { super(); this.rows = []; this.cols = []; + this.settingValues = {}; + for (const setting of this.getSettings()) { + this.settingValues[setting.id] = setting.default; + } + // The object renders to this.rootElem, but we want buttons outside // the datagrid. This actualRoot is just a div containing those buttons // and the rendered rootElem so we can 'adopt' the buttons into this class. @@ -145,6 +158,12 @@ export abstract class InteractiveDataGrid extends DataGri return this.actualRoot; } + protected getSettingValue(settingId: string): boolean { + return this.settingValues[settingId]; + } + + protected abstract getSettings(): DatagridSetting[]; + protected abstract getDefaultOptions(): ColumnOption[]; protected abstract getAvailableOptions(): ColumnOption[]; @@ -157,6 +176,26 @@ export abstract class InteractiveDataGrid extends DataGri private createRoot() { const root = document.createElement('div'); + root.style.width = '100%'; + + // Add settings + for (const setting of this.getSettings()) { + const checkbox = document.createElement('vscode-checkbox') as HTMLInputElement; + if (this.settingValues[setting.id]) { + checkbox.setAttribute('checked', 'true'); + } + checkbox.textContent = setting.text; + checkbox.addEventListener('change', (_) => { + this.settingValues[setting.id] = checkbox.checked; + + this.update(); + }); + + root.appendChild(checkbox); + } + + root.appendChild(document.createElement('vscode-divider')); + root.appendChild(document.createElement('br')); // Add column button const addColBtn = document.createElement('vscode-button'); @@ -291,6 +330,10 @@ export abstract class InteractiveDataGrid extends DataGri this.render(); } + update() { + this.render(); + } + render() { this.clearRows(); diff --git a/views/digitaljs/src/viewers/stats/elements/explorer.ts b/views/digitaljs/src/viewers/stats/elements/explorer.ts index 6bb2087..574b89c 100644 --- a/views/digitaljs/src/viewers/stats/elements/explorer.ts +++ b/views/digitaljs/src/viewers/stats/elements/explorer.ts @@ -1,6 +1,6 @@ import {type Module, type ModuleStatId, getModuleStatIds, getModuleStatName} from '../modules'; -import {type DataGridCell, InteractiveDataGrid} from './datagrid'; +import {type DataGridCell, type DatagridSetting, InteractiveDataGrid} from './datagrid'; import {getPercentage} from './util'; interface ModuleExplorerRowCurrent { @@ -89,6 +89,10 @@ export class ModuleExplorerGrid extends InteractiveDataGrid Date: Sun, 22 Oct 2023 14:45:59 +0200 Subject: [PATCH 25/31] Better support for stat viewer grid headers --- .../src/viewers/stats/elements/datagrid.ts | 45 +++++++++++++------ .../src/viewers/stats/elements/explorer.ts | 18 ++++---- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements/datagrid.ts b/views/digitaljs/src/viewers/stats/elements/datagrid.ts index cdf2cc0..6b51d47 100644 --- a/views/digitaljs/src/viewers/stats/elements/datagrid.ts +++ b/views/digitaljs/src/viewers/stats/elements/datagrid.ts @@ -131,7 +131,7 @@ export abstract class InteractiveDataGrid extends DataGri private rows: RowItem[]; private cols: ColumnOption[]; - private actualRoot: HTMLElement; + private actualRoot: HTMLElement | null; private settingValues: Record; @@ -140,24 +140,34 @@ export abstract class InteractiveDataGrid extends DataGri this.rows = []; this.cols = []; + this.actualRoot = null; this.settingValues = {}; for (const setting of this.getSettings()) { this.settingValues[setting.id] = setting.default; } - // The object renders to this.rootElem, but we want buttons outside - // the datagrid. This actualRoot is just a div containing those buttons - // and the rendered rootElem so we can 'adopt' the buttons into this class. - this.actualRoot = this.createRoot(); + // The object renders to this.rootElem, but we want elements outside + // the datagrid. This actualRoot is just a div containing those elements + // and the rendered rootElem so we can 'adopt' them into this class. + this.actualRoot = null; this.reset(true, true); } override get element(): HTMLElement { + if (this.actualRoot === null) { + this.actualRoot = this.createRoot(); + } return this.actualRoot; } + // *** Start overrideable methods *** + + update() { + this.render(); + } + protected getSettingValue(settingId: string): boolean { return this.settingValues[settingId]; } @@ -174,9 +184,8 @@ export abstract class InteractiveDataGrid extends DataGri protected abstract getValue(item: RowItem, option: ColumnOption): DataGridCell; - private createRoot() { + protected getGridHeaderElement(): HTMLElement { const root = document.createElement('div'); - root.style.width = '100%'; // Add settings for (const setting of this.getSettings()) { @@ -194,8 +203,10 @@ export abstract class InteractiveDataGrid extends DataGri root.appendChild(checkbox); } - root.appendChild(document.createElement('vscode-divider')); - root.appendChild(document.createElement('br')); + if (this.getSettings().length !== 0) { + root.appendChild(document.createElement('vscode-divider')); + root.appendChild(document.createElement('br')); + } // Add column button const addColBtn = document.createElement('vscode-button'); @@ -217,6 +228,18 @@ export abstract class InteractiveDataGrid extends DataGri }); root.appendChild(resetBtn); + return root; + } + + // *** End overrideable methods *** + + private createRoot() { + const root = document.createElement('div'); + root.style.width = '100%'; + + // Custom content + root.appendChild(this.getGridHeaderElement()); + // The actual DataGrid root.appendChild(super.element); @@ -330,10 +353,6 @@ export abstract class InteractiveDataGrid extends DataGri this.render(); } - update() { - this.render(); - } - render() { this.clearRows(); diff --git a/views/digitaljs/src/viewers/stats/elements/explorer.ts b/views/digitaljs/src/viewers/stats/elements/explorer.ts index 574b89c..60ba5ad 100644 --- a/views/digitaljs/src/viewers/stats/elements/explorer.ts +++ b/views/digitaljs/src/viewers/stats/elements/explorer.ts @@ -18,7 +18,6 @@ type ModuleExplorerRowItems = ModuleExplorerRowCurrent | ModuleExplorerRowPrimit type ModuleExplorerOptions = 'name' | 'count' | ModuleStatId; export class ModuleExplorerGrid extends InteractiveDataGrid { - private actualRoot2: HTMLElement; private breadcrumbHeader: HTMLParagraphElement; private moduleBreadcrumbs: Module[]; @@ -27,23 +26,24 @@ export class ModuleExplorerGrid extends InteractiveDataGrid Date: Sun, 22 Oct 2023 14:56:37 +0200 Subject: [PATCH 26/31] Add module count to primitives overview --- views/digitaljs/src/viewers/stats/elements/primitives.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements/primitives.ts b/views/digitaljs/src/viewers/stats/elements/primitives.ts index c8a7299..6f95276 100644 --- a/views/digitaljs/src/viewers/stats/elements/primitives.ts +++ b/views/digitaljs/src/viewers/stats/elements/primitives.ts @@ -4,7 +4,7 @@ import {type DataGridCell, type DatagridSetting, InteractiveDataGrid} from './da import {getPercentage} from './util'; // TODO: typing for primitives (needs exhaustive list) -type PrimitivesOverviewOptions = 'name' | string; +type PrimitivesOverviewOptions = 'name' | 'count' | string; export class PrimitivesOverviewGrid extends InteractiveDataGrid { private modules: Module[]; @@ -27,7 +27,7 @@ export class PrimitivesOverviewGrid extends InteractiveDataGrid Date: Sun, 22 Oct 2023 16:03:40 +0200 Subject: [PATCH 27/31] Add more settings to interactive data grids --- .../digitaljs/src/viewers/stats/elements/datagrid.ts | 1 + .../digitaljs/src/viewers/stats/elements/explorer.ts | 10 +++++++--- .../src/viewers/stats/elements/moduleoverview.ts | 12 ++++++++++-- .../src/viewers/stats/elements/primitives.ts | 9 +++++++-- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements/datagrid.ts b/views/digitaljs/src/viewers/stats/elements/datagrid.ts index 6b51d47..0d37c2a 100644 --- a/views/digitaljs/src/viewers/stats/elements/datagrid.ts +++ b/views/digitaljs/src/viewers/stats/elements/datagrid.ts @@ -201,6 +201,7 @@ export abstract class InteractiveDataGrid extends DataGri }); root.appendChild(checkbox); + root.appendChild(document.createElement('br')); } if (this.getSettings().length !== 0) { diff --git a/views/digitaljs/src/viewers/stats/elements/explorer.ts b/views/digitaljs/src/viewers/stats/elements/explorer.ts index 60ba5ad..349bcc0 100644 --- a/views/digitaljs/src/viewers/stats/elements/explorer.ts +++ b/views/digitaljs/src/viewers/stats/elements/explorer.ts @@ -90,7 +90,7 @@ export class ModuleExplorerGrid extends InteractiveDataGrid Date: Sat, 28 Oct 2023 15:55:04 +0200 Subject: [PATCH 28/31] Correctly save and restore grid table configs --- .../src/viewers/stats/elements/datagrid.ts | 99 ++++++++++++++----- .../src/viewers/stats/elements/explorer.ts | 6 +- .../viewers/stats/elements/moduleoverview.ts | 8 +- .../src/viewers/stats/elements/primitives.ts | 23 ++++- views/digitaljs/src/viewers/stats/index.ts | 44 ++++----- 5 files changed, 124 insertions(+), 56 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements/datagrid.ts b/views/digitaljs/src/viewers/stats/elements/datagrid.ts index 0d37c2a..ac3038d 100644 --- a/views/digitaljs/src/viewers/stats/elements/datagrid.ts +++ b/views/digitaljs/src/viewers/stats/elements/datagrid.ts @@ -118,11 +118,16 @@ export interface DatagridSetting { default: boolean; } -interface GridHeadersUpdateEvent { - newHeaders: ColumnOption[]; +export interface InteractiveDatagridConfig { + columns: ColumnOption[] | null; + settingValues: Record | null; +} + +interface GridConfigUpdateEvent { + config: InteractiveDatagridConfig; } interface InteractiveDataGridEvents { - gridHeadersUpdate: GridHeadersUpdateEvent; + gridConfigUpdate: GridConfigUpdateEvent; } export abstract class InteractiveDataGrid extends DataGrid< @@ -133,7 +138,7 @@ export abstract class InteractiveDataGrid extends DataGri private actualRoot: HTMLElement | null; - private settingValues: Record; + private settingElems: Map; constructor() { super(); @@ -142,9 +147,20 @@ export abstract class InteractiveDataGrid extends DataGri this.cols = []; this.actualRoot = null; - this.settingValues = {}; + // Create setting checkboxes (store references) + this.settingElems = new Map(); for (const setting of this.getSettings()) { - this.settingValues[setting.id] = setting.default; + const checkbox = document.createElement('vscode-checkbox') as HTMLInputElement; + checkbox.addEventListener('change', (_) => { + this.dispatchEvent('gridConfigUpdate', {config: this.getConfig()}); + + this.update(); + }); + if (setting.default) { + checkbox.setAttribute('checked', 'true'); + } + checkbox.textContent = setting.text; + this.settingElems.set(setting.id, checkbox); } // The object renders to this.rootElem, but we want elements outside @@ -162,15 +178,59 @@ export abstract class InteractiveDataGrid extends DataGri return this.actualRoot; } + getConfig(): InteractiveDatagridConfig { + return { + columns: this.cols, + settingValues: Object.fromEntries( + Array.from(this.settingElems.entries()).map(([id, elem]) => [id, elem.checked]) + ) + }; + } + + setConfig(config?: InteractiveDatagridConfig) { + if (!config) { + return; + } + + if (config.columns) { + this.reset(true, false); + + for (const col of config.columns) { + this.addCol(col); + } + } + + for (const [key, value] of Object.entries(config.settingValues ?? {})) { + this.setSetting(key, value); + } + + this.update(); + } + + protected getSetting(settingId: string): boolean { + return this.settingElems.get(settingId)?.checked || false; + } + + protected setSetting(settingId: string, value: boolean) { + const settingElem = this.settingElems.get(settingId); + if (!settingElem) { + return; + } + + if (value) { + settingElem.setAttribute('checked', 'true'); + } else { + settingElem.removeAttribute('checked'); + } + } + // *** Start overrideable methods *** update() { this.render(); } - protected getSettingValue(settingId: string): boolean { - return this.settingValues[settingId]; - } + abstract getIdentifier(): string; protected abstract getSettings(): DatagridSetting[]; @@ -188,18 +248,7 @@ export abstract class InteractiveDataGrid extends DataGri const root = document.createElement('div'); // Add settings - for (const setting of this.getSettings()) { - const checkbox = document.createElement('vscode-checkbox') as HTMLInputElement; - if (this.settingValues[setting.id]) { - checkbox.setAttribute('checked', 'true'); - } - checkbox.textContent = setting.text; - checkbox.addEventListener('change', (_) => { - this.settingValues[setting.id] = checkbox.checked; - - this.update(); - }); - + for (const checkbox of this.settingElems.values()) { root.appendChild(checkbox); root.appendChild(document.createElement('br')); } @@ -215,7 +264,7 @@ export abstract class InteractiveDataGrid extends DataGri addColBtn.addEventListener('click', (_ev) => { this.addCol(); - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + this.dispatchEvent('gridConfigUpdate', {config: this.getConfig()}); }); root.appendChild(addColBtn); @@ -225,7 +274,7 @@ export abstract class InteractiveDataGrid extends DataGri resetBtn.addEventListener('click', (_ev) => { this.reset(true, false); - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + this.dispatchEvent('gridConfigUpdate', {config: this.getConfig()}); }); root.appendChild(resetBtn); @@ -293,7 +342,7 @@ export abstract class InteractiveDataGrid extends DataGri this.delColumn(coli); this.render(); - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + this.dispatchEvent('gridConfigUpdate', {config: this.getConfig()}); } }); header.appendChild(delBtn); @@ -319,7 +368,7 @@ export abstract class InteractiveDataGrid extends DataGri this.cols.splice(headerIndex - this.getDefaultOptions().length, 1, newOption); this.render(); - this.dispatchEvent('gridHeadersUpdate', {newHeaders: this.cols}); + this.dispatchEvent('gridConfigUpdate', {config: this.getConfig()}); }); const pos = super.addColumn([header]); diff --git a/views/digitaljs/src/viewers/stats/elements/explorer.ts b/views/digitaljs/src/viewers/stats/elements/explorer.ts index 349bcc0..8822647 100644 --- a/views/digitaljs/src/viewers/stats/elements/explorer.ts +++ b/views/digitaljs/src/viewers/stats/elements/explorer.ts @@ -32,6 +32,10 @@ export class ModuleExplorerGrid extends InteractiveDataGrid { + const conf = super.getConfig(); + conf.columns = []; + return conf; + } + setConfig(config?: InteractiveDatagridConfig | undefined): void { + if (config) { + config.columns = null; + } + super.setConfig(config); + } + protected getSettings(): DatagridSetting[] { return [ {id: 'count-recursive', text: 'Count submodules recursively', default: true}, @@ -59,13 +76,13 @@ export class PrimitivesOverviewGrid extends InteractiveDataGrid { handleForeignViewMessage(message: ForeignViewMessage): void { @@ -44,28 +41,13 @@ export class StatsViewer extends BaseViewer { } this.moduleOverview = new ModuleOverviewGrid(this.modules); - this.moduleOverview.addEventListener('gridHeadersUpdate', (data) => { - this.storeValue('yosys-stats-overview-settings', {columns: data.newHeaders}); - }); - this.getValue('yosys-stats-overview-settings').then((value) => { - const settings = Object.keys(value).length ? (value as GridColumnSettings) : {columns: []}; - for (const col of settings.columns) { - this.moduleOverview.addCol(col); - } - }); - - this.primitivesOverview = new PrimitivesOverviewGrid(this.modules); + this.setupConfigStore(this.moduleOverview); this.moduleExplorer = new ModuleExplorerGrid(this.modules[0]); - this.moduleExplorer.addEventListener('gridHeadersUpdate', (data) => { - this.storeValue('yosys-stats-explorer-settings', {columns: data.newHeaders}); - }); - this.getValue('yosys-stats-explorer-settings').then((value) => { - const settings = Object.keys(value).length ? (value as GridColumnSettings) : {columns: []}; - for (const col of settings.columns) { - this.moduleExplorer.addCol(col); - } - }); + this.setupConfigStore(this.moduleExplorer); + + this.primitivesOverview = new PrimitivesOverviewGrid(this.modules); + this.setupConfigStore(this.primitivesOverview); this.tabsContainer = new TabsContainer([ {title: 'Overview', element: this.moduleOverview}, @@ -74,6 +56,18 @@ export class StatsViewer extends BaseViewer { ]); } + private setupConfigStore(element: InteractiveDataGrid) { + const storeId = `yosys.stats.${element.getIdentifier()}.settings`; + + // Register callbacks for config back-up + element.addEventListener('gridConfigUpdate', (event) => this.storeValue(storeId, event.config)); + + // Restore initial config + this.getValue(storeId).then((value) => { + element.setConfig(value as InteractiveDatagridConfig); + }); + } + private getModule(name: string): Module | null { for (const module of this.modules) { if (module.name === name) { From d6a54b9bcd798b339b691b4b143f25daee21445c Mon Sep 17 00:00:00 2001 From: Mike A Date: Sun, 29 Oct 2023 12:54:54 +0100 Subject: [PATCH 29/31] Add warning message when missing broadcast targets --- src/extension/editors/yosys.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/extension/editors/yosys.ts b/src/extension/editors/yosys.ts index 53da35f..9175325 100644 --- a/src/extension/editors/yosys.ts +++ b/src/extension/editors/yosys.ts @@ -59,6 +59,11 @@ export class YosysEditor extends BaseEditor { }); return true; } else if (message.type === 'broadcast') { + if (YosysEditor.activeViews.size < 2) { + // There are no views to broadcast to... + vscode.window.showErrorMessage('You must have more than one tab open for this feature to work!'); + } + for (const view of YosysEditor.activeViews) { if (view === webview) { continue; From 040ef8ff554cd973b7f3f8927cd9925ef51bb84c Mon Sep 17 00:00:00 2001 From: Mike A Date: Sun, 29 Oct 2023 13:41:03 +0100 Subject: [PATCH 30/31] Focus explorer tab when called from digitaljs --- .../src/viewers/stats/elements/tabs.ts | 39 +++++++++++++----- views/digitaljs/src/viewers/stats/index.ts | 40 ++++++++++--------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/views/digitaljs/src/viewers/stats/elements/tabs.ts b/views/digitaljs/src/viewers/stats/elements/tabs.ts index aaa8c91..5d9d897 100644 --- a/views/digitaljs/src/viewers/stats/elements/tabs.ts +++ b/views/digitaljs/src/viewers/stats/elements/tabs.ts @@ -1,6 +1,7 @@ import {CustomElement} from './base'; interface Tab { + id: string; title: string; element: CustomElement; } @@ -8,12 +9,17 @@ interface Tab { export class TabsContainer extends CustomElement> { protected rootElem: HTMLElement; - private tabs: Tab[]; + private tabs: Map>; + private tabHeaders: Map; constructor(tabs: Tab[]) { super(); - this.tabs = tabs; + this.tabs = new Map(); + for (const tab of tabs) { + this.tabs.set(tab.id, tab); + } + this.tabHeaders = new Map(); this.rootElem = this.createRoot(); } @@ -21,24 +27,35 @@ export class TabsContainer extends CustomElement> { private createRoot(): HTMLElement { const root = document.createElement('vscode-panels'); - for (let i = 0; i < this.tabs.length; i++) { - const tab = document.createElement('vscode-panel-tab'); - tab.id = `tab-${i}`; - tab.textContent = this.tabs[i].title; - root.appendChild(tab); + for (const [id, tab] of this.tabs.entries()) { + const tabElem = document.createElement('vscode-panel-tab'); + tabElem.id = `tab-${id}`; + tabElem.textContent = tab.title; + root.appendChild(tabElem); + + this.tabHeaders.set(id, tabElem); } - for (let i = 0; i < this.tabs.length; i++) { + for (const [id, tab] of this.tabs.entries()) { const view = document.createElement('vscode-panel-view'); - view.id = `view-${i}`; - view.appendChild(this.tabs[i].element.element); + view.id = `view-${id}`; + view.appendChild(tab.element.element); root.appendChild(view); } return root; } + focusTab(id: string) { + const tabHeader = this.tabHeaders.get(id); + if (!tabHeader) { + throw new Error('Invalid tab ID to focus!'); + } + + tabHeader.click(); + } + render(): void { - for (const tab of this.tabs) { + for (const tab of this.tabs.values()) { tab.element.render(); } } diff --git a/views/digitaljs/src/viewers/stats/index.ts b/views/digitaljs/src/viewers/stats/index.ts index cb8679a..e33760d 100644 --- a/views/digitaljs/src/viewers/stats/index.ts +++ b/views/digitaljs/src/viewers/stats/index.ts @@ -8,22 +8,6 @@ import type {InteractiveDataGrid, InteractiveDatagridConfig} from './elements/da import {type Module, buildModuleTree} from './modules'; export class StatsViewer extends BaseViewer { - handleForeignViewMessage(message: ForeignViewMessage): void { - if (message.type === 'moduleFocus') { - // Focus explorer navigator according to breadcrumbs from djs viewer - this.moduleExplorer.navigateSplice(1); - for (const moduleName of message.breadcrumbs) { - const module = this.getModule(moduleName); - if (!module) { - throw new Error(`Unknown module: ${moduleName}`); - } - this.moduleExplorer.navigate(module); - } - - this.moduleExplorer.update(); - } - } - private modules: Module[]; private tabsContainer: TabsContainer; @@ -50,12 +34,30 @@ export class StatsViewer extends BaseViewer { this.setupConfigStore(this.primitivesOverview); this.tabsContainer = new TabsContainer([ - {title: 'Overview', element: this.moduleOverview}, - {title: 'Explorer', element: this.moduleExplorer}, - {title: 'Primitives', element: this.primitivesOverview} + {id: 'overview', title: 'Overview', element: this.moduleOverview}, + {id: 'explorer', title: 'Explorer', element: this.moduleExplorer}, + {id: 'primitives', title: 'Primitives', element: this.primitivesOverview} ]); } + handleForeignViewMessage(message: ForeignViewMessage): void { + if (message.type === 'moduleFocus') { + // Focus explorer navigator according to breadcrumbs from djs viewer + this.moduleExplorer.navigateSplice(1); + for (const moduleName of message.breadcrumbs) { + const module = this.getModule(moduleName); + if (!module) { + throw new Error(`Unknown module: ${moduleName}`); + } + this.moduleExplorer.navigate(module); + } + + this.moduleExplorer.update(); + + this.tabsContainer.focusTab('explorer'); + } + } + private setupConfigStore(element: InteractiveDataGrid) { const storeId = `yosys.stats.${element.getIdentifier()}.settings`; From 5b6248839fb4d1a209d0bd0d0f66f607520b76ca Mon Sep 17 00:00:00 2001 From: Danielle Huisman Date: Sun, 26 Nov 2023 14:12:44 +0100 Subject: [PATCH 31/31] Enable type checked ESLint config --- .eslintrc.json | 8 +++- package-lock.json | 8 ++-- package.json | 4 +- scripts/run-each-view.ts | 56 +++++++++++----------- src/common/universal-worker.ts | 4 ++ src/extension/commands/actions.ts | 8 ++-- src/extension/commands/base.ts | 2 +- src/extension/commands/project.ts | 28 ++++++----- src/extension/editors/base.ts | 22 ++++----- src/extension/editors/nextpnr.ts | 16 +++---- src/extension/editors/project.ts | 20 ++++---- src/extension/editors/yosys.ts | 31 ++++++------ src/extension/index.ts | 5 +- src/extension/projects/project.ts | 30 ++++++------ src/extension/projects/projects.ts | 10 ++-- src/extension/tasks/messaging.ts | 3 ++ src/extension/tasks/nextpnr.ts | 2 +- src/extension/tasks/rtl.ts | 7 +-- src/extension/tasks/runner.ts | 7 ++- src/extension/tasks/terminal.ts | 11 +++-- src/extension/tasks/yosys.ts | 4 +- src/extension/test/suite/extension.test.ts | 1 + src/extension/trees/files.ts | 2 +- src/extension/trees/projects.ts | 2 +- src/extension/util.ts | 6 +-- src/extension/webview.ts | 4 +- src/extension/webviews/actions.ts | 19 +++++--- src/workers/worker.ts | 5 +- tsconfig.json | 2 +- 29 files changed, 175 insertions(+), 152 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index edc381d..520c948 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,11 @@ { "root": true, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended-type-checked"], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], + "parserOptions": { + "project": true + }, "rules": { "@typescript-eslint/consistent-type-imports": [ "warn", @@ -11,6 +14,7 @@ "fixStyle": "inline-type-imports" } ], - "@typescript-eslint/no-unused-vars": ["warn", {"argsIgnorePattern": "^_"}] + "@typescript-eslint/no-unused-vars": ["warn", {"argsIgnorePattern": "^_"}], + "@typescript-eslint/require-await": "off" } } diff --git a/package-lock.json b/package-lock.json index ab9945c..364fb1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "process": "^0.11.10", "ts-loader": "^9.5.1", "ts-node": "^10.9.1", - "typescript": "^5.3.2", + "typescript": "5.2.2", "webpack": "^5.89.0", "webpack-cli": "^5.1.4" }, @@ -5046,9 +5046,9 @@ } }, "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 9e0fe45..4e18509 100644 --- a/package.json +++ b/package.json @@ -280,7 +280,7 @@ "compile-web": "npm run build-views && tsc && webpack", "package-web": "npm run build-views && tsc && webpack --mode production --devtool hidden-source-map", "vscode:prepublish": "npm run package-web", - "lint": "eslint ./src ./workers/src --ext ts", + "lint": "eslint ./src --ext ts", "test": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/extension/test/suite/index.js", "pretest": "npm run compile-web" }, @@ -301,7 +301,7 @@ "process": "^0.11.10", "ts-loader": "^9.5.1", "ts-node": "^10.9.1", - "typescript": "^5.3.2", + "typescript": "5.2.2", "webpack": "^5.89.0", "webpack-cli": "^5.1.4" }, diff --git a/scripts/run-each-view.ts b/scripts/run-each-view.ts index 4d3901b..d5f8a40 100644 --- a/scripts/run-each-view.ts +++ b/scripts/run-each-view.ts @@ -1,38 +1,40 @@ import {spawnSync} from 'child_process'; import {readdir} from 'fs/promises'; import path from 'path'; -import {fileURLToPath} from 'url'; -const command = process.argv.slice(2); -const commandName = ['npm'].concat(command).join(' '); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + const command = process.argv.slice(2); + const commandName = ['npm'].concat(command).join(' '); -const currentDirectory = path.dirname(fileURLToPath(import.meta.url)); -const viewsDirectory = path.resolve(currentDirectory, '..', 'views'); -const failedDirectories: string[] = []; + const currentDirectory = path.dirname(__filename); + const viewsDirectory = path.resolve(currentDirectory, '..', 'views'); + const failedDirectories: string[] = []; -console.log(`Executing command "${commandName}" for all views.`); -console.log(); + console.log(`Executing command "${commandName}" for all views.`); + console.log(); -for (const directory of await readdir(viewsDirectory)) { - const directoryName = `./views/${directory}`; + for (const directory of await readdir(viewsDirectory)) { + const directoryName = `./views/${directory}`; - console.log(`Executing command "${commandName}" in views "${directoryName}"`); + console.log(`Executing command "${commandName}" in views "${directoryName}"`); - const {status} = spawnSync('npm', command, { - cwd: path.resolve(viewsDirectory, directory), - stdio: ['inherit', 'inherit', 'inherit'] - }); - console.log(); + const {status} = spawnSync('npm', command, { + cwd: path.resolve(viewsDirectory, directory), + stdio: ['inherit', 'inherit', 'inherit'] + }); + console.log(); + + if (status !== 0) { + failedDirectories.push(directoryName); + } + } - if (status !== 0) { - failedDirectories.push(directoryName); + if (failedDirectories.length === 0) { + console.log(`Executing command "${commandName}" was successful for all views.`); + } else { + console.log(`Executing command "${commandName}" failed for these views:`); + console.log(failedDirectories.map((directory) => ` - ${directory}`).join('\n')); + console.log('See the logs above for more details.'); } -} - -if (failedDirectories.length === 0) { - console.log(`Executing command "${commandName}" was successful for all views.`); -} else { - console.log(`Executing command "${commandName}" failed for these views:`); - console.log(failedDirectories.map((directory) => ` - ${directory}`).join('\n')); - console.log('See the logs above for more details.'); -} +})(); diff --git a/src/common/universal-worker.ts b/src/common/universal-worker.ts index 5304ded..b0108da 100644 --- a/src/common/universal-worker.ts +++ b/src/common/universal-worker.ts @@ -7,8 +7,10 @@ type WorkerThreadsModule = typeof import('worker_threads'); type WorkerThreadsWorker = import('worker_threads').Worker; /* eslint-enable @typescript-eslint/consistent-type-imports */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ const module: WorkerThreadsModule | undefined = typeof Worker === 'undefined' ? __non_webpack_require__('worker_threads') : undefined; +/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ type InternalNodeWorker = { type: 'node'; @@ -40,6 +42,7 @@ export const onEvent = (event: E, callback: Even if (module) { module.parentPort?.on(event, callback); } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument addEventListener(event, (event) => callback(extractData(event))); } }; @@ -77,6 +80,7 @@ export class UniversalWorker { public onEvent(event: E, callback: EventCallbacks[E]): void { if (this.worker.type === 'web') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.worker.worker.addEventListener(event, (event) => callback(extractData(event))); } else { this.worker.worker.on(event, callback); diff --git a/src/extension/commands/actions.ts b/src/extension/commands/actions.ts index 6229b84..437552c 100644 --- a/src/extension/commands/actions.ts +++ b/src/extension/commands/actions.ts @@ -13,7 +13,7 @@ export class OpenProjectConfigurationCommand extends CurrentProjectCommand { async executeForCurrentProject(project: Project) { // Open project file - vscode.commands.executeCommand('vscode.openWith', project.getUri(), ProjectEditor.getViewType()); + await vscode.commands.executeCommand('vscode.openWith', project.getUri(), ProjectEditor.getViewType()); } } @@ -31,14 +31,14 @@ abstract class RunTaskCommand extends CurrentProjectCommand { return false; } - const uri = vscode.Uri.joinPath(task.scope.uri, task.definition.project); + const uri = vscode.Uri.joinPath(task.scope.uri, task.definition.project as string); return uri.toString() === project.getUri().toString(); }); if (task) { - vscode.tasks.executeTask(task); + await vscode.tasks.executeTask(task); } else { - vscode.window.showErrorMessage('No task could be found for the current EDA project.'); + await vscode.window.showErrorMessage('No task could be found for the current EDA project.'); } } } diff --git a/src/extension/commands/base.ts b/src/extension/commands/base.ts index e74e0c0..ed71744 100644 --- a/src/extension/commands/base.ts +++ b/src/extension/commands/base.ts @@ -24,7 +24,7 @@ export abstract class CurrentProjectCommand extends BaseCommand { async execute(...args: unknown[]) { const project = this.projects.getCurrent(); if (!project) { - vscode.window.showWarningMessage('No EDA project selected.'); + await vscode.window.showWarningMessage('No EDA project selected.'); return; } diff --git a/src/extension/commands/project.ts b/src/extension/commands/project.ts index 9da73e3..137557e 100644 --- a/src/extension/commands/project.ts +++ b/src/extension/commands/project.ts @@ -112,10 +112,13 @@ export class NewProjectCommand extends BaseCommand { // Check if the project is within the workspace folder const [workspaceRelativePath] = getWorkspaceRelativePath(projectWorkspace, projectUri); if (!workspaceRelativePath) { - vscode.window.showErrorMessage('Selected project location must be within the selected workspace folder.', { - detail: `File "${projectUri.path}" is not in folder "${projectWorkspace.path}".`, - modal: true - }); + await vscode.window.showErrorMessage( + 'Selected project location must be within the selected workspace folder.', + { + detail: `File "${projectUri.path}" is not in folder "${projectWorkspace.path}".`, + modal: true + } + ); return; } @@ -126,7 +129,7 @@ export class NewProjectCommand extends BaseCommand { await this.projects.add(projectUri, true, true); // Open project file - vscode.commands.executeCommand('vscode.openWith', projectUri, ProjectEditor.getViewType()); + await vscode.commands.executeCommand('vscode.openWith', projectUri, ProjectEditor.getViewType()); } } @@ -187,10 +190,13 @@ export class OpenProjectCommand extends BaseCommand { // Check if the project is within the workspace folder const [workspaceRelativePath] = getWorkspaceRelativePath(projectWorkspace, projectUri); if (!workspaceRelativePath) { - vscode.window.showErrorMessage('Selected project location must be within the selected workspace folder.', { - detail: `File "${projectUri.path}" is not in folder "${projectWorkspace.path}".`, - modal: true - }); + await vscode.window.showErrorMessage( + 'Selected project location must be within the selected workspace folder.', + { + detail: `File "${projectUri.path}" is not in folder "${projectWorkspace.path}".`, + modal: true + } + ); return; } @@ -198,7 +204,7 @@ export class OpenProjectCommand extends BaseCommand { await this.projects.add(projectUri, true, false); // Open project file - vscode.commands.executeCommand('vscode.openWith', projectUri, ProjectEditor.getViewType()); + await vscode.commands.executeCommand('vscode.openWith', projectUri, ProjectEditor.getViewType()); } } @@ -218,6 +224,6 @@ export class SelectProject extends BaseCommand { } async execute(project: Project) { - await this.projects.setCurrent(project); + this.projects.setCurrent(project); } } diff --git a/src/extension/editors/base.ts b/src/extension/editors/base.ts index d9fec84..eaced3c 100644 --- a/src/extension/editors/base.ts +++ b/src/extension/editors/base.ts @@ -73,21 +73,21 @@ export abstract class BaseEditor extends BaseWebview implemen this.update(document, webview, false); } - protected onDidReceiveMessage( + protected async onDidReceiveMessage( _document: vscode.TextDocument, webview: vscode.Webview, message: ViewMessage | GlobalStoreMessage - ): boolean { + ): Promise { if (message.type === 'globalStore') { if (message.action === 'set') { - this.context.globalState.update(message.name, message.value).then(() => { - const response: GlobalStoreMessage = { - type: 'globalStore', - action: 'result', - transaction: message.transaction - }; - webview.postMessage(response); - }); + await this.context.globalState.update(message.name, message.value); + const response: GlobalStoreMessage = { + type: 'globalStore', + action: 'result', + transaction: message.transaction + }; + await webview.postMessage(response); + return true; } else if (message.action === 'get') { const value = this.context.globalState.get(message.name) || ({} as object); @@ -97,7 +97,7 @@ export abstract class BaseEditor extends BaseWebview implemen transaction: message.transaction, result: value }; - webview.postMessage(response); + await webview.postMessage(response); return true; } diff --git a/src/extension/editors/nextpnr.ts b/src/extension/editors/nextpnr.ts index 390f186..880f1d6 100644 --- a/src/extension/editors/nextpnr.ts +++ b/src/extension/editors/nextpnr.ts @@ -29,22 +29,22 @@ export class NextpnrEditor extends BaseEditor { @font-face { font-family: "codicon"; font-display: block; - src: url("${fontUri}") format("truetype"); + src: url("${fontUri.toString()}") format("truetype"); } `; } - protected onDidReceiveMessage( + protected async onDidReceiveMessage( document: vscode.TextDocument, webview: vscode.Webview, message: ViewMessage | GlobalStoreMessage - ): boolean { - if (super.onDidReceiveMessage(document, webview, message)) { + ): Promise { + if (await super.onDidReceiveMessage(document, webview, message)) { return true; } if (message.type === 'ready') { - webview.postMessage({ + await webview.postMessage({ type: 'document', document: document.getText() }); @@ -62,12 +62,12 @@ export class NextpnrEditor extends BaseEditor { // Do nothing } - protected update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { + protected async update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { if (!isDocumentChange) { - vscode.commands.executeCommand('edacation-projects.focus'); + await vscode.commands.executeCommand('edacation-projects.focus'); } - webview.postMessage({ + await webview.postMessage({ type: 'document', document: document.getText() }); diff --git a/src/extension/editors/project.ts b/src/extension/editors/project.ts index 56c8058..c3551d8 100644 --- a/src/extension/editors/project.ts +++ b/src/extension/editors/project.ts @@ -34,12 +34,12 @@ export class ProjectEditor extends BaseEditor { } } - protected onDidReceiveMessage( + protected async onDidReceiveMessage( document: vscode.TextDocument, webview: vscode.Webview, message: ViewMessage | GlobalStoreMessage - ): boolean { - if (super.onDidReceiveMessage(document, webview, message)) { + ): Promise { + if (await super.onDidReceiveMessage(document, webview, message)) { return true; } if (message.type === 'ready') { @@ -48,7 +48,7 @@ export class ProjectEditor extends BaseEditor { console.log('ready project', document.uri); if (project) { - webview.postMessage({ + await webview.postMessage({ type: 'project', project: Project.serialize(project) }); @@ -60,8 +60,8 @@ export class ProjectEditor extends BaseEditor { } const edit = new vscode.WorkspaceEdit(); - edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), message.document as string); - vscode.workspace.applyEdit(edit); + edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), message.document); + await vscode.workspace.applyEdit(edit); return true; } @@ -73,26 +73,26 @@ export class ProjectEditor extends BaseEditor { // Reload project await this.projects.reload(document.uri); - this.update(document, webview, false); + await this.update(document, webview, false); } protected onClose(_document: vscode.TextDocument, _webview: vscode.Webview): void { // Do nothing } - protected update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { + protected async update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { if (isDocumentChange) { return; } - vscode.commands.executeCommand('edacation-projects.focus'); + await vscode.commands.executeCommand('edacation-projects.focus'); const project = this.projects.get(document.uri); console.log('updating project', project); if (project) { - webview.postMessage({ + await webview.postMessage({ type: 'project', project: Project.serialize(project) }); diff --git a/src/extension/editors/yosys.ts b/src/extension/editors/yosys.ts index 4a36132..3c13af0 100644 --- a/src/extension/editors/yosys.ts +++ b/src/extension/editors/yosys.ts @@ -37,23 +37,23 @@ export class YosysEditor extends BaseEditor { @font-face { font-family: "codicon"; font-display: block; - src: url("${fontUri}") format("truetype"); + src: url("${fontUri.toString()}") format("truetype"); } `; } - protected onDidReceiveMessage( + protected async onDidReceiveMessage( document: vscode.TextDocument, webview: vscode.Webview, message: ViewMessage | GlobalStoreMessage - ): boolean { - if (super.onDidReceiveMessage(document, webview, message)) { + ): Promise { + if (await super.onDidReceiveMessage(document, webview, message)) { return true; } if (message.type === 'ready') { - webview.postMessage({ + await webview.postMessage({ type: 'document', document: document.getText() }); @@ -61,14 +61,14 @@ export class YosysEditor extends BaseEditor { } else if (message.type === 'broadcast') { if (YosysEditor.activeViews.size < 2) { // There are no views to broadcast to... - vscode.window.showErrorMessage('You must have more than one tab open for this feature to work!'); + await vscode.window.showErrorMessage('You must have more than one tab open for this feature to work!'); } for (const view of YosysEditor.activeViews) { if (view === webview) { continue; } - view.postMessage(message); + await view.postMessage(message); } return true; } else if (message.type === 'requestSave') { @@ -76,15 +76,14 @@ export class YosysEditor extends BaseEditor { const projectRoot = util.findProjectRoot(document.uri) || util.getParentUri(document.uri); const path = vscode.Uri.joinPath(projectRoot, message.data.defaultPath || ''); - util.offerSaveFile(message.data.fileContents, { + const uri = await util.offerSaveFile(message.data.fileContents, { defaultUri: path, filters: message.data?.filters - }).then((path) => { - if (!path) { - return; - } - this.showSaveNotification(path); }); + if (uri) { + await this.showSaveNotification(uri); + } + return true; } @@ -99,14 +98,14 @@ export class YosysEditor extends BaseEditor { YosysEditor.activeViews.delete(webview); } - protected update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { + protected async update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) { YosysEditor.activeViews.add(webview); if (!isDocumentChange) { - vscode.commands.executeCommand('edacation-projects.focus'); + await vscode.commands.executeCommand('edacation-projects.focus'); } - webview.postMessage({ + await webview.postMessage({ type: 'document', document: document.getText() }); diff --git a/src/extension/index.ts b/src/extension/index.ts index 4ed2c7e..518ff78 100644 --- a/src/extension/index.ts +++ b/src/extension/index.ts @@ -42,12 +42,9 @@ export const activate = async (context: vscode.ExtensionContext) => { // Register webview providers for (const webviewType of Object.values(webviews)) { const webview = new webviewType(context, projects); - context.subscriptions.push( - vscode.window.registerWebviewViewProvider(webviewType.getViewID(), webview) - ); + context.subscriptions.push(vscode.window.registerWebviewViewProvider(webviewType.getViewID(), webview)); } - await projects.load(); }; diff --git a/src/extension/projects/project.ts b/src/extension/projects/project.ts index 4d93d08..7c847a7 100644 --- a/src/extension/projects/project.ts +++ b/src/extension/projects/project.ts @@ -1,4 +1,4 @@ -import {Project as BaseProject, DEFAULT_CONFIGURATION, type ProjectConfiguration} from 'edacation'; +import {Project as BaseProject, DEFAULT_CONFIGURATION, type ProjectConfiguration, type ProjectState} from 'edacation'; import path from 'path'; import * as vscode from 'vscode'; @@ -82,7 +82,7 @@ export class Project extends BaseProject { for (const fileUri of fileUris) { const [workspaceRelativePath, folderRelativePath] = getWorkspaceRelativePath(this.getRoot(), fileUri); if (!workspaceRelativePath) { - vscode.window.showErrorMessage(`File must be in the a subfolder of the EDA project root.`, { + await vscode.window.showErrorMessage(`File must be in the a subfolder of the EDA project root.`, { detail: `File "${fileUri.path}" is not in folder "${this.getRoot().path}".`, modal: true }); @@ -95,7 +95,7 @@ export class Project extends BaseProject { } } - await super.addInputFiles(filePaths); + super.addInputFiles(filePaths); this.projects.emitInputFileChange(); @@ -107,7 +107,7 @@ export class Project extends BaseProject { this.inputFileUris.delete(filePath); } - await super.removeInputFiles(filePaths); + super.removeInputFiles(filePaths); this.projects.emitInputFileChange(); @@ -119,7 +119,7 @@ export class Project extends BaseProject { for (const fileUri of fileUris) { const [workspaceRelativePath, folderRelativePath] = getWorkspaceRelativePath(this.getRoot(), fileUri); if (!workspaceRelativePath) { - vscode.window.showErrorMessage(`File must be in the a subfolder of the EDA project root.`, { + await vscode.window.showErrorMessage(`File must be in the a subfolder of the EDA project root.`, { detail: `File "${fileUri.path}" is not in folder "${this.getRoot().path}".`, modal: true }); @@ -132,7 +132,7 @@ export class Project extends BaseProject { } } - await super.addOutputFiles(filePaths); + super.addOutputFiles(filePaths); this.projects.emitOutputFileChange(); @@ -144,7 +144,7 @@ export class Project extends BaseProject { this.outputFileUris.delete(filePath); } - await super.removeOutputFiles(filePaths); + super.removeOutputFiles(filePaths); this.projects.emitOutputFileChange(); @@ -155,24 +155,22 @@ export class Project extends BaseProject { await Project.store(this); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static serialize(project: Project): any { + static serialize(project: Project): ProjectState { return BaseProject.serialize(project); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static deserialize(data: any, projects: Projects, uri: vscode.Uri): Project { - const name: string = data.name; - const inputFiles: string[] = data.inputFiles ?? []; - const outputFiles: string[] = data.outputFiles ?? []; - const configuration: ProjectConfiguration = data.configuration ?? {}; + static deserialize(data: ProjectState, projects: Projects, uri: vscode.Uri): Project { + const name = data.name; + const inputFiles = data.inputFiles ?? []; + const outputFiles = data.outputFiles ?? []; + const configuration = data.configuration ?? ({} as ProjectConfiguration); return new Project(projects, uri, name, inputFiles, outputFiles, configuration); } static async load(projects: Projects, uri: vscode.Uri): Promise { const data = decodeJSON(await vscode.workspace.fs.readFile(uri)); - const project = Project.deserialize(data, projects, uri); + const project = Project.deserialize(data as ProjectState, projects, uri); return project; } diff --git a/src/extension/projects/projects.ts b/src/extension/projects/projects.ts index 82c49f7..58b1743 100644 --- a/src/extension/projects/projects.ts +++ b/src/extension/projects/projects.ts @@ -120,7 +120,7 @@ export class Projects { this.projects.push(project); } } catch (err) { - this.context.workspaceState.update('projects', undefined); + await this.context.workspaceState.update('projects', undefined); throw err; } @@ -170,7 +170,7 @@ export class Projects { return this.currentProject; } - async setCurrent(project: Project) { + setCurrent(project: Project) { const previousProject = this.currentProject; this.currentProject = project; @@ -188,7 +188,7 @@ export class Projects { this.emitOutputFileChange(); } - handleTaskEnd(event: vscode.TaskEndEvent) { + async handleTaskEnd(event: vscode.TaskEndEvent) { const task = event.execution.task; if (['yosys-rtl', 'yosys-synth', 'nextpnr'].includes(task.definition.type)) { @@ -200,8 +200,8 @@ export class Projects { return; } - const uri = vscode.Uri.joinPath(task.scope.uri, task.definition.project); - this.reload(uri); + const uri = vscode.Uri.joinPath(task.scope.uri, task.definition.project as string); + await this.reload(uri); } } } diff --git a/src/extension/tasks/messaging.ts b/src/extension/tasks/messaging.ts index 049833a..6266028 100644 --- a/src/extension/tasks/messaging.ts +++ b/src/extension/tasks/messaging.ts @@ -1,3 +1,5 @@ +import type {Uri} from 'vscode'; + export enum AnsiModifier { RESET = '\x1b[0m', BOLD = '\x1b[1m', @@ -8,6 +10,7 @@ export enum AnsiModifier { export interface TaskOutputFile { path: string; data?: Uint8Array; + uri?: Uri; } export interface TerminalMessagePrintln { diff --git a/src/extension/tasks/nextpnr.ts b/src/extension/tasks/nextpnr.ts index 8c5ee74..fd679e0 100644 --- a/src/extension/tasks/nextpnr.ts +++ b/src/extension/tasks/nextpnr.ts @@ -91,7 +91,7 @@ class NextpnrTerminalTask extends TerminalTask { const pnrFile = outputFiles.find((file) => file.path === 'routed.nextpnr.json'); if (pnrFile) { const uri = vscode.Uri.joinPath(project.getRoot(), pnrFile.path); - vscode.commands.executeCommand('vscode.open', uri); + await vscode.commands.executeCommand('vscode.open', uri); } } } diff --git a/src/extension/tasks/rtl.ts b/src/extension/tasks/rtl.ts index af5a53b..51d8288 100644 --- a/src/extension/tasks/rtl.ts +++ b/src/extension/tasks/rtl.ts @@ -1,6 +1,7 @@ import {type YosysWorkerOptions, encodeText, generateYosysRTLCommands} from 'edacation'; import {basename} from 'path-browserify'; import * as vscode from 'vscode'; +import type {Uri} from 'vscode'; import type {MessageFile} from '../../common/messages.js'; import type {Project} from '../projects/index.js'; @@ -74,14 +75,14 @@ class RTLTerminalTask extends BaseYosysTerminalTask { } async handleEnd(project: Project, outputFiles: TaskOutputFile[]) { - super.handleEnd(project, outputFiles); + await super.handleEnd(project, outputFiles); // Update file contents so our custom editor can handle them // TODO: print the message to stderr as an error (red text). // This is implemented in https://github.com/EDAcation/vscode-edacation/pull/14. await Promise.all( outputFiles.map((file) => - this.updateFile(file.uri).catch((_err) => + this.updateFile(file.uri as Uri).catch((_err) => this.println(`Error while updating "${file.path}"; the file might not be usable!`) ) ) @@ -91,7 +92,7 @@ class RTLTerminalTask extends BaseYosysTerminalTask { const rtlFile = outputFiles.find((file) => file.path.endsWith('rtl.digitaljs.json')); if (rtlFile) { const uri = vscode.Uri.joinPath(project.getRoot(), rtlFile.path); - vscode.commands.executeCommand('vscode.open', uri); + await vscode.commands.executeCommand('vscode.open', uri); } } } diff --git a/src/extension/tasks/runner.ts b/src/extension/tasks/runner.ts index c9f7bb1..be2bcba 100644 --- a/src/extension/tasks/runner.ts +++ b/src/extension/tasks/runner.ts @@ -159,14 +159,13 @@ export class NativeTaskRunner extends TaskRunner { proc.on('exit', this.onProcessExit.bind(this, ctx)); proc.on('error', this.onProcessError.bind(this)); - proc.stdout.on('data', (data) => this.onProcessData(data, 'stdout')); - proc.stderr.on('data', (data) => this.onProcessData(data, 'stderr')); + proc.stdout.on('data', (data) => this.onProcessData(data as string, 'stdout')); + proc.stderr.on('data', (data) => this.onProcessData(data as string, 'stderr')); } private onProcessError(error: unknown) { if (error instanceof Error) { - // ugly, but it's a nodejs error so there's no typings for the code attribute - if (Object(error)?.code === 'ENOENT') { + if ((error as Error & {code: string}).code === 'ENOENT') { this.error( 'Could not find native runner entrypoint. Are Yosys/Nextpnr installed on your system and available in PATH?' ); diff --git a/src/extension/tasks/terminal.ts b/src/extension/tasks/terminal.ts index 886051c..c064696 100644 --- a/src/extension/tasks/terminal.ts +++ b/src/extension/tasks/terminal.ts @@ -45,8 +45,8 @@ export abstract class TaskProvider extends BaseTaskProvider { return this.getTask( task.scope, - task.definition.uri, - task.definition.project, + task.definition.uri as vscode.Uri, + task.definition.project as string, task.definition as TaskDefinition ); } @@ -131,8 +131,8 @@ export class TaskTerminal implements vscode.Pseudoterminal { this.logMessages = []; } - open() { - this.execute(); + async open() { + await this.execute(); } close() { @@ -194,10 +194,11 @@ export class TaskTerminal implements vscode.Pseudoterminal { this.definition.uri || vscode.Uri.joinPath(this.folder.uri, this.definition.project) ); } catch (err) { - this.error(err); + await this.error(err); return; } + // eslint-disable-next-line @typescript-eslint/no-misused-promises this.task.onMessage(this.handleMessage.bind(this, project)); try { diff --git a/src/extension/tasks/yosys.ts b/src/extension/tasks/yosys.ts index aec6cdb..5ade02d 100644 --- a/src/extension/tasks/yosys.ts +++ b/src/extension/tasks/yosys.ts @@ -100,13 +100,13 @@ export class YosysTaskProvider extends TaskProvider { class YosysTerminalTask extends BaseYosysTerminalTask { async handleEnd(project: Project, outputFiles: TaskOutputFile[]) { - super.handleEnd(project, outputFiles); + await super.handleEnd(project, outputFiles); // Open LUT file in DigitalJS editor const lutFile = outputFiles.find((file) => file.path.endsWith('luts.digitaljs.json')); if (lutFile) { const uri = vscode.Uri.joinPath(project.getRoot(), lutFile.path); - vscode.commands.executeCommand('vscode.open', uri); + await vscode.commands.executeCommand('vscode.open', uri); } } } diff --git a/src/extension/test/suite/extension.test.ts b/src/extension/test/suite/extension.test.ts index feabf57..81d6a82 100644 --- a/src/extension/test/suite/extension.test.ts +++ b/src/extension/test/suite/extension.test.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; // import * as myExtension from '../../extension'; suite('Web Extension Test Suite', () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises vscode.window.showInformationMessage('Start all tests.'); test('Sample test', () => { diff --git a/src/extension/trees/files.ts b/src/extension/trees/files.ts index 6daf94a..66e9b60 100644 --- a/src/extension/trees/files.ts +++ b/src/extension/trees/files.ts @@ -23,7 +23,7 @@ abstract class FilesProvider extends BaseTreeDataProvider { }; } - async getChildren(element?: ProjectFile): Promise { + getChildren(element?: ProjectFile): ProjectFile[] { const project = this.projects.getCurrent(); if (!project) { return []; diff --git a/src/extension/trees/projects.ts b/src/extension/trees/projects.ts index 10a0c2e..744d67e 100644 --- a/src/extension/trees/projects.ts +++ b/src/extension/trees/projects.ts @@ -28,7 +28,7 @@ export class ProjectProvider extends BaseTreeDataProvider { }; } - async getChildren(element?: Project): Promise { + getChildren(element?: Project): Project[] { if (!element) { return this.projects.getAll(); } diff --git a/src/extension/util.ts b/src/extension/util.ts index 83f1126..e967217 100644 --- a/src/extension/util.ts +++ b/src/extension/util.ts @@ -29,7 +29,7 @@ export const ensureFile = async (uri: vscode.Uri, exists: boolean): Promise textEncoder.encode(input.endsWith(' export const encodeJSON = (input: unknown, pretty = false) => encodeText(JSON.stringify(input, undefined, pretty ? 4 : undefined)); -export const decodeText = (input: BufferSource) => textDecoder.decode(input); -export const decodeJSON = (input: BufferSource) => JSON.parse(decodeText(input)); +export const decodeText = (input: BufferSource): string => textDecoder.decode(input); +export const decodeJSON = (input: BufferSource): unknown => JSON.parse(decodeText(input)); export const FILE_EXTENSIONS_VERILOG = ['v', 'vh', 'sv', 'svh']; export const FILE_EXTENSIONS_VHDL = ['vhd']; diff --git a/src/extension/webview.ts b/src/extension/webview.ts index 81c9800..e0dc2e8 100644 --- a/src/extension/webview.ts +++ b/src/extension/webview.ts @@ -46,7 +46,7 @@ export abstract class BaseWebview> { const styleUris = stylePaths.map((stylePath) => getWebviewUri(webview, this.context, stylePath)); return styleUris - .map((styleUri) => /*html*/ ``) + .map((styleUri) => /*html*/ ``) .join('\n'); } @@ -55,7 +55,7 @@ export abstract class BaseWebview> { const scriptUris = scriptPaths.map((scriptPath) => getWebviewUri(webview, this.context, scriptPath)); return scriptUris - .map((scriptUri) => /*html*/ ``) + .map((scriptUri) => /*html*/ ``) .join('\n'); } diff --git a/src/extension/webviews/actions.ts b/src/extension/webviews/actions.ts index 68bdff0..764dd42 100644 --- a/src/extension/webviews/actions.ts +++ b/src/extension/webviews/actions.ts @@ -1,9 +1,9 @@ import type * as vscode from 'vscode'; +import {Project} from '../projects/index.js'; import type {ViewMessage} from '../types.js'; import {BaseWebviewViewProvider} from './base.js'; -import {Project} from '../projects/index.js'; export class ActionsProvider extends BaseWebviewViewProvider { public static getViewID() { @@ -26,26 +26,31 @@ export class ActionsProvider extends BaseWebviewViewProvider { }; } - public resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): void | Thenable { - super.resolveWebviewView(webviewView, context, token) + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + token: vscode.CancellationToken + ): void | Thenable { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + super.resolveWebviewView(webviewView, context, token); // TODO: subscribe to project change // this.projects.getProjectEmitter().event. } - protected onDidReceiveMessage(webview: vscode.Webview, message: ViewMessage): void { + protected async onDidReceiveMessage(webview: vscode.Webview, message: ViewMessage): Promise { console.log('[actions]', message); if (message.type === 'ready') { - const project = this.projects.getCurrent() + const project = this.projects.getCurrent(); if (!project) { - return + return; } console.log('[actions]', 'ready project', project.getUri()); if (project) { - webview.postMessage({ + await webview.postMessage({ type: 'project', project: Project.serialize(project) }); diff --git a/src/workers/worker.ts b/src/workers/worker.ts index 7cc0a85..6a3c425 100644 --- a/src/workers/worker.ts +++ b/src/workers/worker.ts @@ -29,6 +29,7 @@ export abstract class WorkerTool { constructor() { this.toolPromise = this.initialize(); + // eslint-disable-next-line @typescript-eslint/no-misused-promises onEvent('message', this.handleMessage.bind(this)); onEvent('messageerror', this.handleMessageError.bind(this)); } @@ -133,7 +134,9 @@ export abstract class WorkerTool { // TODO: create output directories // Execute Emscripten tool - // @ts-expect-error: TODO + + // @ts-expect-error TODO + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access tool.getModule().callMain(message.args); // Read output files diff --git a/tsconfig.json b/tsconfig.json index 0a9adba..985e81b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,5 @@ "noUnusedLocals": true, "noUnusedParameters": true }, - "exclude": ["node_modules", "scripts", "views"] + "exclude": ["node_modules", "views"] }