From a9492ad86dc929e75e7380e4212994127abb1f71 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 8 Oct 2024 17:38:22 +0000 Subject: [PATCH] Implement watch list management. --- example/.vscode/settings.json | 7 +- package.json | 42 +++++++++++- src/debug/session.ts | 11 +++ src/debug/watch.ts | 44 ++++++++++++ src/extension.ts | 13 +++- src/ui/sidebar.ts | 122 +++++++++++++++++++++++++++++++--- 6 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 src/debug/watch.ts diff --git a/example/.vscode/settings.json b/example/.vscode/settings.json index e222d61..5ba2a58 100644 --- a/example/.vscode/settings.json +++ b/example/.vscode/settings.json @@ -1,3 +1,6 @@ { - "rtlDebugger.command": ["${workspaceFolder}/design_sim"] -} \ No newline at end of file + "rtlDebugger.command": [ + "${workspaceFolder}/design_sim" + ], + "rtlDebugger.watchList": [] +} diff --git a/package.json b/package.json index 212efca..8eed0d4 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ "$comment": "UPSTREAM: Unfortunately there is no way to control the formatting of the extension name within the setting title. See microsoft/vscode#103592", "properties": { "rtlDebugger.command": { - "type": [ - "array" - ], + "type": "array", "items": { "type": "string" }, @@ -60,6 +58,20 @@ ], "default": "Verilog", "markdownDescription": "Specifies the display format for variables." + }, + "rtlDebugger.watchList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "row": {"type": "integer"}, + "bit": {"type": "integer"} + }, + "required": ["id"], + "additionalProperties": false + }, + "description": "Specifies the list of variables being watched separately." } } }, @@ -104,6 +116,18 @@ "category": "RTL Debugger", "title": "Step Backward", "icon": "$(debug-step-back)" + }, + { + "command": "rtlDebugger.watchVariable", + "category": "RTL Debugger", + "title": "Watch Variable", + "icon": "$(eye-watch)" + }, + { + "command": "rtlDebugger.unWatchVariable", + "category": "RTL Debugger", + "title": "Stop Watching", + "icon": "$(remove)" } ], "viewsContainers": { @@ -197,6 +221,18 @@ "when": "view == rtlDebugger.sidebar && rtlDebugger.sessionStatus == running", "group": "navigation@5" } + ], + "view/item/context": [ + { + "command": "rtlDebugger.watchVariable", + "when": "view == rtlDebugger.sidebar && viewItem == canWatch", + "group": "inline" + }, + { + "command": "rtlDebugger.unWatchVariable", + "when": "view == rtlDebugger.sidebar && viewItem == inWatchList", + "group": "inline" + } ] } }, diff --git a/src/debug/session.ts b/src/debug/session.ts index 4233c31..d6d5712 100644 --- a/src/debug/session.ts +++ b/src/debug/session.ts @@ -116,6 +116,17 @@ export class Session { ); } + async getVariable(variableIdentifier: string): Promise { + const identifierParts = variableIdentifier.split(' '); + const scopeIdentifier = identifierParts.slice(0, identifierParts.length - 1).join(' '); + const items = await this.listItemsInScope(scopeIdentifier); + if (variableIdentifier in items) { + return Variable.fromCXXRTL(variableIdentifier, items[variableIdentifier]); + } else { + return null; + } + } + // ======================================== Querying the database private referenceEpochs: Map = new Map(); diff --git a/src/debug/watch.ts b/src/debug/watch.ts new file mode 100644 index 0000000..b5afecc --- /dev/null +++ b/src/debug/watch.ts @@ -0,0 +1,44 @@ +import * as vscode from 'vscode'; + +export interface IWatchItem { + id: string; + row?: number; + bit?: number; +} + +export interface IWatchList { + get(): IWatchItem[]; + set(items: IWatchItem[]): void; + append(item: IWatchItem): void; + remove(index: number): void; + + onDidChange(callback: (items: IWatchItem[]) => any): vscode.Disposable; +} + +export const watchList: IWatchList = { + get(): IWatchItem[] { + return vscode.workspace.getConfiguration('rtlDebugger').get('watchList') || []; + }, + + set(items: IWatchItem[]): void { + vscode.workspace.getConfiguration('rtlDebugger').update('watchList', items); + }, + + append(item: IWatchItem): void { + this.set(this.get().concat(item)); + }, + + remove(index: number): void { + const items = this.get(); + items.splice(index, 1); + this.set(items); + }, + + onDidChange(callback: (items: IWatchItem[]) => any): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('rtlDebugger.watchList')) { + callback(watchList.get()); + } + }); + }, +}; diff --git a/src/extension.ts b/src/extension.ts index a19a135..fa11155 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,15 +1,17 @@ import * as vscode from 'vscode'; +import { watchList } from './debug/watch'; import { CXXRTLDebugger } from './debugger'; import * as sidebar from './ui/sidebar'; import { inputTime } from './ui/input'; export function activate(context: vscode.ExtensionContext) { const rtlDebugger = new CXXRTLDebugger(); - const sidebarTreeDataProvider = new sidebar.TreeDataProvider(rtlDebugger); - context.subscriptions.push(vscode.window.createTreeView('rtlDebugger.sidebar', { + const sidebarTreeDataProvider = new sidebar.TreeDataProvider(rtlDebugger); + const sidebarTreeView = vscode.window.createTreeView('rtlDebugger.sidebar', { treeDataProvider: sidebarTreeDataProvider - })); + }); + context.subscriptions.push(sidebarTreeView); vscode.commands.executeCommand('setContext', 'rtlDebugger.sessionStatus', rtlDebugger.sessionStatus); context.subscriptions.push(rtlDebugger.onDidChangeSessionStatus((state) => @@ -48,6 +50,11 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.stepForward', () => rtlDebugger.session!.stepForward())); + context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.watchVariable', (treeItem) => + watchList.append(treeItem.getWatchItem()))); + context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.unWatchVariable', (treeItem) => + watchList.remove(treeItem.metadata.index))); + // For an unknown reason, the `vscode.open` command (which does the exact same thing) ignores the options. context.subscriptions.push(vscode.commands.registerCommand('rtlDebugger.openDocument', (uri: vscode.Uri, options: vscode.TextDocumentShowOptions) => { diff --git a/src/ui/sidebar.ts b/src/ui/sidebar.ts index 011572a..b99e5de 100644 --- a/src/ui/sidebar.ts +++ b/src/ui/sidebar.ts @@ -6,9 +6,13 @@ import { DisplayStyle, variableDescription, variableBitIndices, memoryRowIndices import { CXXRTLDebugger } from '../debugger'; import { Observer } from '../debug/observer'; import { Designation, MemoryRangeDesignation, MemoryRowDesignation, ScalarDesignation } from '../model/sample'; +import { IWatchItem, watchList } from '../debug/watch'; import { Session } from '../debug/session'; abstract class TreeItem { + // Currently used only for removing watch items, where knowing the index is necessary. + metadata: any; + constructor( readonly provider: TreeDataProvider ) {} @@ -33,6 +37,7 @@ class BitTreeItem extends TreeItem { provider: TreeDataProvider, readonly designation: ScalarDesignation | MemoryRowDesignation, readonly bitIndex: number, + readonly contextValue?: string, ) { super(provider); } @@ -58,14 +63,25 @@ class BitTreeItem extends TreeItem { : '= 0'; } treeItem.tooltip = variableTooltip(variable); + treeItem.contextValue = this.contextValue; return treeItem; } + + getWatchItem(): IWatchItem { + return { + id: this.designation.variable.cxxrtlIdentifier, + row: (this.designation instanceof MemoryRowDesignation) + ? this.designation.index : undefined, + bit: this.bitIndex, + }; + } } class ScalarTreeItem extends TreeItem { constructor( provider: TreeDataProvider, readonly designation: ScalarDesignation | MemoryRowDesignation, + readonly contextValue?: string, ) { super(provider); } @@ -88,13 +104,22 @@ class ScalarTreeItem extends TreeItem { treeItem.description += variableValue(this.displayStyle, variable, value); treeItem.tooltip = variableTooltip(variable); treeItem.command = variable.location?.asOpenCommand(); + treeItem.contextValue = this.contextValue; return treeItem; } override getChildren(): TreeItem[] { const variable = this.designation.variable; return Array.from(variableBitIndices(this.displayStyle, variable)).map((index) => - new BitTreeItem(this.provider, this.designation, index)); + new BitTreeItem(this.provider, this.designation, index, 'canWatch')); + } + + getWatchItem(): IWatchItem { + return { + id: this.designation.variable.cxxrtlIdentifier, + row: (this.designation instanceof MemoryRowDesignation) + ? this.designation.index : undefined, + }; } } @@ -102,6 +127,7 @@ class ArrayTreeItem extends TreeItem { constructor( provider: TreeDataProvider, readonly designation: MemoryRangeDesignation, + readonly contextValue?: string, ) { super(provider); } @@ -114,13 +140,20 @@ class ArrayTreeItem extends TreeItem { treeItem.description = variableDescription(this.displayStyle, variable); treeItem.tooltip = variableTooltip(variable); treeItem.command = variable.location?.asOpenCommand(); + treeItem.contextValue = this.contextValue; return treeItem; } override getChildren(): TreeItem[] { const variable = this.designation.variable; return Array.from(memoryRowIndices(variable)).map((index) => - new ScalarTreeItem(this.provider, variable.designation(index))); + new ScalarTreeItem(this.provider, variable.designation(index), 'canWatch')); + } + + getWatchItem(): IWatchItem { + return { + id: this.designation.variable.cxxrtlIdentifier + }; } } @@ -164,10 +197,61 @@ class ScopeTreeItem extends TreeItem { } for (const variable of await this.scope.variables) { if (variable instanceof ScalarVariable) { - children.push(new ScalarTreeItem(this.provider, variable.designation())); + children.push(new ScalarTreeItem(this.provider, variable.designation(), 'canWatch')); } if (variable instanceof MemoryVariable) { - children.push(new ArrayTreeItem(this.provider, variable.designation())); + children.push(new ArrayTreeItem(this.provider, variable.designation(), 'canWatch')); + } + } + return children; + } +} + +class WatchTreeItem extends TreeItem { + constructor( + provider: TreeDataProvider + ) { + super(provider); + } + + override async getTreeItem(): Promise { + if (watchList.get().length > 0) { + return new vscode.TreeItem('Watch', vscode.TreeItemCollapsibleState.Expanded); + } else { + return new vscode.TreeItem('Watch (empty)'); + } + } + + override async getChildren(): Promise { + const children = []; + for (const [index, watchItem] of watchList.get().entries()) { + const variable = await this.provider.getVariable(watchItem.id); + if (variable === null) { + continue; + } + let designation; + if (variable instanceof ScalarVariable) { + designation = new ScalarDesignation(variable); + } else if (variable instanceof MemoryVariable) { + if (watchItem.row !== undefined) { + designation = new MemoryRowDesignation(variable, watchItem.row); + } else { + designation = new MemoryRangeDesignation(variable, 0, variable.depth); + } + } + let treeItem; + if (designation instanceof MemoryRangeDesignation) { + treeItem = new ArrayTreeItem(this.provider, designation, 'inWatchList'); + } else if (designation instanceof ScalarDesignation || designation instanceof MemoryRowDesignation) { + if (watchItem.bit === undefined) { + treeItem = new ScalarTreeItem(this.provider, designation, 'inWatchList'); + } else { + treeItem = new BitTreeItem(this.provider, designation, watchItem.bit, 'inWatchList'); + } + } + if (treeItem !== undefined) { + treeItem.metadata = { index }; + children.push(treeItem); } } return children; @@ -180,6 +264,8 @@ export class TreeDataProvider implements vscode.TreeDataProvider { private session: Session | null = null; private observer: Observer | null = null; + private watchTreeItem: WatchTreeItem | null = null; + private scopeTreeItem: ScopeTreeItem | null = null; constructor(rtlDebugger: CXXRTLDebugger) { vscode.workspace.onDidChangeConfiguration((event) => { @@ -187,16 +273,25 @@ export class TreeDataProvider implements vscode.TreeDataProvider { this._onDidChangeTreeData.fire(null); } }); - rtlDebugger.onDidChangeSession((session) => { + rtlDebugger.onDidChangeSession(async (session) => { this.session = session; if (session !== null) { this.observer = new Observer(session, 'sidebar'); + this.watchTreeItem = new WatchTreeItem(this); + this.scopeTreeItem = new ScopeTreeItem(this, await session.getRootScope()); } else { this.observer?.dispose(); this.observer = null; + this.watchTreeItem = null; + this.scopeTreeItem = null; } this._onDidChangeTreeData.fire(null); }); + watchList.onDidChange((_items) => { + if (this.watchTreeItem !== null) { + this._onDidChangeTreeData.fire(this.watchTreeItem); + } + }); } getTreeItem(element: TreeItem): vscode.TreeItem | Thenable { @@ -207,13 +302,14 @@ export class TreeDataProvider implements vscode.TreeDataProvider { if (element !== undefined) { return await element.getChildren(); } - if (this.session !== null) { - return [ - new ScopeTreeItem(this, await this.session.getRootScope()), - ]; - } else { - return []; + const children = []; + if (this.watchTreeItem !== null) { + children.push(this.watchTreeItem); } + if (this.scopeTreeItem !== null) { + children.push(this.scopeTreeItem); + } + return children; } get displayStyle(): DisplayStyle { @@ -221,6 +317,10 @@ export class TreeDataProvider implements vscode.TreeDataProvider { return displayStyle as DisplayStyle; } + getVariable(identifier: string): Promise { + return this.session!.getVariable(identifier); + } + getValue(element: TreeItem, designation: Designation): T | undefined { if (this.observer === null) { return;