From f308ed82be167f1e21daba8548362e5cd016f4d9 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Mon, 11 May 2026 11:57:35 +0200 Subject: [PATCH 01/17] cpuStates: (re-)store states in settings.json and add a global command to reset all settings --- package.json | 23 ++++++- src/cbuild-run/cbuild-run-reader.ts | 3 + src/desktop/extension.ts | 8 +++ .../cpu-states/cpu-states-statusbar-item.ts | 14 ++++- src/features/cpu-states/cpu-states.ts | 60 ++++++++++++++++--- 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index d27b3e67..07d91d21 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,11 @@ ] }, "commands": [ + { + "command": "vscode-cmsis-debugger.resetDynamicViewState", + "title": "Reset All Dynamic View States", + "category": "CMSIS Debugger" + }, { "command": "vscode-cmsis-debugger.openDisassemblyView", "title": "Open Disassembly View", @@ -274,6 +279,10 @@ } ], "commandPalette": [ + { + "command": "vscode-cmsis-debugger.resetDynamicViewState", + "when": "true" + }, { "command": "vscode-cmsis-debugger.openDisassemblyView", "when": "false" @@ -546,7 +555,19 @@ } } } - ] + ], + "configuration": { + "title": "Arm CMSIS Debugger", + "properties": { + "vscode-cmsis-debugger.cpuStates.viewState": { + "type": "object", + "markdownDescription": "Persisted CPU time enable/disable state per debug configuration.", + "additionalProperties": { + "type": "boolean" + } + } + } + } }, "scripts": { "prepare": "npm run build", diff --git a/src/cbuild-run/cbuild-run-reader.ts b/src/cbuild-run/cbuild-run-reader.ts index 00b2d323..463bbee4 100644 --- a/src/cbuild-run/cbuild-run-reader.ts +++ b/src/cbuild-run/cbuild-run-reader.ts @@ -108,4 +108,7 @@ export class CbuildRunReader { return pnameProcessors.map(p => p.pname!); } + public getTargetType(): string | undefined { + return this.cbuildRun?.['target-type']; + } } diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index f94e8b52..d0234d47 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -85,6 +85,14 @@ export const activate = async (context: vscode.ExtensionContext): Promise canCompleteActivation = false; } + // Register reset dynamic view state command + context.subscriptions.push( vscode.commands.registerCommand("vscode-cmsis-debugger.resetDynamicViewState", async () => { + await Promise.all([ + cpuStates.resetViewState(), + ]); + }) + ); + if (!canCompleteActivation) { logger.debug('CMSIS Debugger activation incomplete'); // Let promise float, we reload the window. diff --git a/src/features/cpu-states/cpu-states-statusbar-item.ts b/src/features/cpu-states/cpu-states-statusbar-item.ts index c26e6efc..7b9ff76b 100644 --- a/src/features/cpu-states/cpu-states-statusbar-item.ts +++ b/src/features/cpu-states/cpu-states-statusbar-item.ts @@ -66,6 +66,7 @@ export class CpuStatesStatusBarItem { } protected async handleItemCommand(): Promise { + const cpuEnabled = this.cpuStates?.isEnabled ?? true; const items: QuickPickHandlerItem[] = [ { label: 'CPU Time', @@ -76,7 +77,18 @@ export class CpuStatesStatusBarItem { label: 'Reset CPU Time', detail: 'Reset CPU execution time and history', handler: () => vscode.commands.executeCommand(CpuStatesCommands.resetCpuTimeHistoryID) - } + }, + cpuEnabled + ? { + label: 'Disable CPU Timer', + detail: 'Stop collecting CPU execution time', + handler: () => vscode.commands.executeCommand(CpuStatesCommands.disableCpuTimer) + } + : { + label: 'Enable CPU Timer', + detail: 'Start collecting CPU execution time', + handler: () => vscode.commands.executeCommand(CpuStatesCommands.enableCpuTimer) + } ]; const selection = await vscode.window.showQuickPick(items); if (!selection) { diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 6002eb26..f610cb5e 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -43,16 +43,19 @@ interface SessionCpuStates { isRunning: boolean; hasStates: boolean|undefined; skipFrequencyUpdate: boolean; + enableCpuStatesFlag: boolean; + configStateKey: string; } export class CpuStates { + private static readonly SETTINGS_KEY = 'vscode-cmsis-debugger.cpuStates.viewState'; + // onRefresh event to notify GUI components of the cpu states updates (This is different than that of the periodic refresh timer) private readonly _onRefresh: vscode.EventEmitter = new vscode.EventEmitter(); public readonly onRefresh: vscode.Event = this._onRefresh.event; public activeSession: GDBTargetDebugSession | undefined; private sessionCpuStates: Map = new Map(); - private enableCpuStatesFlag: boolean = true; public get activeCpuStates(): SessionCpuStates|undefined { if (!this.activeSession) { @@ -80,7 +83,9 @@ export class CpuStates { statesHistory: new CpuStatesHistory(extractPname(session.session.name)), isRunning: true, hasStates: undefined, - skipFrequencyUpdate: false + skipFrequencyUpdate: false, + enableCpuStatesFlag: true, + configStateKey: session.session.configuration.name }; this.sessionCpuStates.set(session.session.id, states); session.refreshTimer.onRefresh(async (refreshSession) => this.handlePeriodicRefresh(refreshSession)); @@ -95,6 +100,9 @@ export class CpuStates { protected handleActiveSessionChanged(session?: GDBTargetDebugSession): void { this.activeSession = session; + // Restore enabled/disabled state of CPU Time commands based on persisted settings + const enabled = session ? (this.sessionCpuStates.get(session.session.id)?.enableCpuStatesFlag ?? true) : false; + vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', enabled); this._onRefresh.fire(0); } @@ -103,6 +111,13 @@ export class CpuStates { if (!cpuStates) { return; } + // cbuild-run is now parsed. Refine the key with the target-type prefix and restore the enabled/disabled state from settings.json + const cbuildRun = await session.getCbuildRun(); + const targetType = cbuildRun?.getTargetType(); + const configStateKey = targetType ? `${targetType}::${session.session.configuration.name}` : session.session.configuration.name; + const existingSettings = vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}; + cpuStates.configStateKey = configStateKey; + cpuStates.enableCpuStatesFlag = existingSettings[configStateKey] ?? true; // Following call might fail if target not stopped on connect, returns undefined // Retry on first Stopped Event. cpuStates.hasStates = await this.supportsCpuStates(session); @@ -199,7 +214,7 @@ export class CpuStates { if (!states) { return; } - if (!this.enableCpuStatesFlag) { + if (!this.activeCpuStates?.enableCpuStatesFlag) { return; } const newCycles = await session.readMemoryU32(DWT_CYCCNT_ADDRESS); @@ -326,14 +341,45 @@ export class CpuStates { this._onRefresh.fire(0); } + public get isEnabled(): boolean { + return this.activeCpuStates?.enableCpuStatesFlag ?? true; + } + public async enableCpuStates(): Promise { - this.enableCpuStatesFlag = true; - await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', this.enableCpuStatesFlag); + const cpuStates = this.activeCpuStates; + if (!cpuStates) { + return; + } + cpuStates.enableCpuStatesFlag = true; + const originalSettings = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; + delete originalSettings[cpuStates.configStateKey]; + const valueToStore = Object.keys(originalSettings).length === 0 ? undefined : originalSettings; + await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, valueToStore, vscode.ConfigurationTarget.Workspace); + await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } public async disableCpuStates(): Promise { - this.enableCpuStatesFlag = false; - await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', this.enableCpuStatesFlag); + const cpuStates = this.activeCpuStates; + if (!cpuStates) { + return; + } + cpuStates.enableCpuStatesFlag = false; + const existingSettings = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; + existingSettings[cpuStates.configStateKey] = false; + await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, existingSettings, vscode.ConfigurationTarget.Workspace); + await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } + public async resetViewState(): Promise { + // Clear all persisted cpu states settings from both workspace/user settings.json + await Promise.all([ + vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace), + vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Global), + ]); + // Re-enable all active sessions in memory + for (const states of this.sessionCpuStates.values()) { + states.enableCpuStatesFlag = true; + } + vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); + } }; From c61e215bea13b9ad334add036f2966c7ef183a52 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Mon, 11 May 2026 15:59:26 +0200 Subject: [PATCH 02/17] componentViewer: (re-)store states in settings.json and add buttons for periodicUpdate --- package.json | 58 +++++++++- src/desktop/extension.ts | 2 + src/features/cpu-states/cpu-states.ts | 17 +-- .../component-viewer/component-viewer-base.ts | 102 ++++++++++++++++++ .../component-viewer/dynamic-view-settings.ts | 53 +++++++++ 5 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 src/views/component-viewer/dynamic-view-settings.ts diff --git a/package.json b/package.json index 07d91d21..495254e3 100644 --- a/package.json +++ b/package.json @@ -183,11 +183,13 @@ { "command": "vscode-cmsis-debugger.componentViewer.enablePeriodicUpdate", "title": "Enable Periodic Update", + "icon": "$(sync)", "category": "Component Viewer" }, { "command": "vscode-cmsis-debugger.componentViewer.disablePeriodicUpdate", "title": "Disable Periodic Update", + "icon": "$(sync-ignored)", "category": "Component Viewer" }, { @@ -223,11 +225,13 @@ { "command": "vscode-cmsis-debugger.corePeripherals.enablePeriodicUpdate", "title": "Enable Periodic Update", + "icon": "$(sync)", "category": "Core Peripherals" }, { "command": "vscode-cmsis-debugger.corePeripherals.disablePeriodicUpdate", "title": "Disable Periodic Update", + "icon": "$(sync-ignored)", "category": "Core Peripherals" }, { @@ -422,13 +426,13 @@ }, { "command": "vscode-cmsis-debugger.componentViewer.expandAll", - "when": "view == cmsis-debugger.componentViewer", - "group": "navigation@3" + "when": "view == cmsis-debugger.componentViewer && componentViewer.sessionActive", + "group": "navigation@4" }, { "command": "vscode-cmsis-debugger.corePeripherals.expandAll", - "when": "view == cmsis-debugger.corePeripherals", - "group": "navigation@3" + "when": "view == cmsis-debugger.corePeripherals && corePeripherals.sessionActive", + "group": "navigation@4" }, { "command": "vscode-cmsis-debugger.componentViewer.filterTree", @@ -449,6 +453,26 @@ "command": "vscode-cmsis-debugger.corePeripherals.clearFilter", "when": "view == cmsis-debugger.corePeripherals && corePeripherals.filterActive", "group": "navigation@2" + }, + { + "command": "vscode-cmsis-debugger.componentViewer.disablePeriodicUpdate", + "when": "view == cmsis-debugger.componentViewer && componentViewer.periodicUpdateEnabled && componentViewer.sessionActive", + "group": "navigation@3" + }, + { + "command": "vscode-cmsis-debugger.componentViewer.enablePeriodicUpdate", + "when": "view == cmsis-debugger.componentViewer && !componentViewer.periodicUpdateEnabled && componentViewer.sessionActive", + "group": "navigation@3" + }, + { + "command": "vscode-cmsis-debugger.corePeripherals.disablePeriodicUpdate", + "when": "view == cmsis-debugger.corePeripherals && corePeripherals.periodicUpdateEnabled && corePeripherals.sessionActive", + "group": "navigation@3" + }, + { + "command": "vscode-cmsis-debugger.corePeripherals.enablePeriodicUpdate", + "when": "view == cmsis-debugger.corePeripherals && !corePeripherals.periodicUpdateEnabled && corePeripherals.sessionActive", + "group": "navigation@3" } ], "view/item/context": [ @@ -565,6 +589,32 @@ "additionalProperties": { "type": "boolean" } + }, + "vscode-cmsis-debugger.componentViewer.viewState": { + "type": "object", + "markdownDescription": "Persisted dynamic view state for the Component Viewer.", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "periodicUpdateEnabled": { "type": "boolean" }, + "filterPattern": { "type": "string" }, + "lockedComponents": { "type": "array", "items": { "type": "string" } } + } + } + }, + "vscode-cmsis-debugger.corePeripherals.viewState": { + "type": "object", + "markdownDescription": "Persisted dynamic view state for the Core Peripherals view.", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "periodicUpdateEnabled": { "type": "boolean" }, + "filterPattern": { "type": "string" }, + "lockedComponents": { "type": "array", "items": { "type": "string" } } + } + } } } } diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index d0234d47..071cad80 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -89,6 +89,8 @@ export const activate = async (context: vscode.ExtensionContext): Promise context.subscriptions.push( vscode.commands.registerCommand("vscode-cmsis-debugger.resetDynamicViewState", async () => { await Promise.all([ cpuStates.resetViewState(), + componentViewer.resetViewState(), + corePeripherals.resetViewState(), ]); }) ); diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index f610cb5e..1d67810a 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -115,9 +115,9 @@ export class CpuStates { const cbuildRun = await session.getCbuildRun(); const targetType = cbuildRun?.getTargetType(); const configStateKey = targetType ? `${targetType}::${session.session.configuration.name}` : session.session.configuration.name; - const existingSettings = vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}; + const storedStates = vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}; cpuStates.configStateKey = configStateKey; - cpuStates.enableCpuStatesFlag = existingSettings[configStateKey] ?? true; + cpuStates.enableCpuStatesFlag = storedStates[configStateKey] ?? true; // Following call might fail if target not stopped on connect, returns undefined // Retry on first Stopped Event. cpuStates.hasStates = await this.supportsCpuStates(session); @@ -351,9 +351,9 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = true; - const originalSettings = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; - delete originalSettings[cpuStates.configStateKey]; - const valueToStore = Object.keys(originalSettings).length === 0 ? undefined : originalSettings; + const statesToStore = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; + delete statesToStore[cpuStates.configStateKey]; + const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, valueToStore, vscode.ConfigurationTarget.Workspace); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } @@ -364,9 +364,9 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = false; - const existingSettings = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; - existingSettings[cpuStates.configStateKey] = false; - await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, existingSettings, vscode.ConfigurationTarget.Workspace); + const storedStates = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; + storedStates[cpuStates.configStateKey] = false; + await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, storedStates, vscode.ConfigurationTarget.Workspace); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } @@ -381,5 +381,6 @@ export class CpuStates { states.enableCpuStatesFlag = true; } vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); + logger.info("CPU States: CPU Timer reset"); } }; diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index 09ef4b85..e522d994 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -25,6 +25,7 @@ import { perf, parsePerf } from './stats-config'; import { vscodeViewExists } from '../../vscode-utils'; import { EXTENSION_NAME, VIEW_PREFIX } from '../../manifest'; import { ExtendedGDBTargetConfiguration } from '../../debug-configuration/gdbtarget-configuration'; +import { readDynamicViewState, writeDynamicViewState, clearAllDynamicViewState, DynamicViewState, DynamicViewStateByConfig } from './dynamic-view-settings'; export interface ScvdCollector { getScvdFilePaths(session: GDBTargetDebugSession): Promise; @@ -52,6 +53,8 @@ export class ComponentViewerBase { private _runningUpdate: boolean = false; private _refreshTimerEnabled: boolean = true; private _activeInputBox: vscode.InputBox | undefined; + private _filterDebounceTimer: NodeJS.Timeout | undefined; + private static readonly filterDebounceMs = 1000; private static readonly pendingUpdateDelayMs = 150; public constructor( @@ -101,10 +104,14 @@ export class ComponentViewerBase { const enablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.enablePeriodicUpdate`, async () => { this._refreshTimerEnabled = true; componentViewerLogger.info(`${this._viewName}: Auto refresh enabled`); + vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); + this.saveCurrentState(); }); const disablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.disablePeriodicUpdate`, async () => { this._refreshTimerEnabled = false; componentViewerLogger.info(`${this._viewName}: Auto refresh disabled`); + vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, false); + this.saveCurrentState(); }); const expandAllCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.expandAll`, async () => { componentViewerLogger.debug(`${this._viewName}: Expand all tree items`); @@ -128,6 +135,7 @@ export class ComponentViewerBase { filterTreeCommandDisposable, clearFilterCommandDisposable ); + vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true) return true; } @@ -223,6 +231,14 @@ export class ComponentViewerBase { this._componentViewerTreeDataProvider.setFilter(value); void vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, true); } + // Reset the timer so the view state is saved only after the user stops typing for filterDebounceMs. + if (this._filterDebounceTimer) { + clearTimeout(this._filterDebounceTimer); + } + this._filterDebounceTimer = setTimeout(() => { + this._filterDebounceTimer = undefined; + this.saveCurrentState(); + }, ComponentViewerBase.filterDebounceMs); }; inputBox.onDidChangeValue(value => { @@ -253,6 +269,12 @@ export class ComponentViewerBase { if (this._activeInputBox) { this._activeInputBox.hide(); } + // Cancel any pending debounced save and persist the cleared state immediately. + if (this._filterDebounceTimer) { + clearTimeout(this._filterDebounceTimer); + this._filterDebounceTimer = undefined; + } + this.saveCurrentState(); } protected async readScvdFiles(tracker: GDBTargetDebugTracker, session?: GDBTargetDebugSession): Promise { @@ -390,6 +412,9 @@ export class ComponentViewerBase { private async handleOnConnected(session: GDBTargetDebugSession, tracker: GDBTargetDebugTracker): Promise { // Update debug session this._activeSession = session; + vscode.commands.executeCommand('setContext', `${this._viewId}.sessionActive`, true); + // Restore persisted view state (filter + periodic update) + await this.restorePeriodicUpdateAndFilter(session); // Load SCVD files from cbuild-run await this.loadScvdFiles(session, tracker); } @@ -408,6 +433,10 @@ export class ComponentViewerBase { private async handleOnDidChangeActiveDebugSession(session: GDBTargetDebugSession | undefined): Promise { // Update debug session this._activeSession = session; + vscode.commands.executeCommand('setContext', `${this._viewId}.sessionActive`, !!session); + if (session) { + await this.restorePeriodicUpdateAndFilter(session); + } } private schedulePendingUpdate(updateReason: UpdateReason): void { @@ -500,4 +529,77 @@ export class ComponentViewerBase { perf?.logSummaries(); this._componentViewerTreeDataProvider.setRoots(roots); } + + private get _settingsKey(): string { + return `${EXTENSION_NAME}.${this._viewId}.viewState`; + } + + private async sessionStateKey(session: GDBTargetDebugSession): Promise { + const cbuildRun = await session.getCbuildRun(); + const targetType = cbuildRun?.getTargetType(); + const configName = session.session.configuration.name; + return targetType ? `${targetType}::${configName}` : configName; + } + + private async saveCurrentState(): Promise { + if (!this._activeSession) { + return; + } + const configStateKey = await this.sessionStateKey(this._activeSession); + const filterPattern = this._componentViewerTreeDataProvider.filterPattern; + // If User settings disable periodicUpdate update but this Workspace/session enables it, + // write true explicitly so the User value does not bleed through. + const inspection = vscode.workspace.getConfiguration().inspect(this._settingsKey); + const userState = inspection?.globalValue?.[configStateKey]; + const needsExplicitPeriodicUpdate = this._refreshTimerEnabled && userState?.periodicUpdateEnabled === false; + const state: DynamicViewState = { + ...(!this._refreshTimerEnabled || needsExplicitPeriodicUpdate ? { periodicUpdateEnabled: this._refreshTimerEnabled } : {}), + ...(filterPattern !== undefined ? { filterPattern } : {}), + }; + await writeDynamicViewState(this._settingsKey, configStateKey, state); + } + + private async restorePeriodicUpdateAndFilter(session: GDBTargetDebugSession): Promise { + // Always reset to defaults before applying saved state to prevent state leaking between sessions + this._refreshTimerEnabled = true; + vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); + this._componentViewerTreeDataProvider.setFilter(undefined); + vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, false); + + const state = readDynamicViewState(this._settingsKey, await this.sessionStateKey(session)); + if (!state) { + return; + } + if (state.periodicUpdateEnabled !== undefined) { + this._refreshTimerEnabled = state.periodicUpdateEnabled; + vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, state.periodicUpdateEnabled); + componentViewerLogger.info(`${this._viewName}: Restored periodicUpdateEnabled=${state.periodicUpdateEnabled}`); + } + if (state.filterPattern !== undefined) { + this._componentViewerTreeDataProvider.setFilter(state.filterPattern); + vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, true); + componentViewerLogger.info(`${this._viewName}: Restored filterPattern='${state.filterPattern}'`); + } + } + + public async resetViewState(): Promise { + // Clear persisted settings + await clearAllDynamicViewState([this._settingsKey]); + // Reset in-memory state to defaults + this._refreshTimerEnabled = true; + vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); + this._componentViewerTreeDataProvider.setFilter(undefined); + vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, false); + // Unlock all locked instances + for (const wrapper of this._instances) { + wrapper.lockState = false; + const guiTree = wrapper.componentViewerInstance.getGuiTree(); + if (guiTree?.length) { + const rootNode: ScvdGuiInterface = guiTree[0]; + rootNode.isLocked = false; + } + } + this.schedulePendingUpdate('sessionChanged'); + componentViewerLogger.info(`${this._viewName}: View state reset`); + } } diff --git a/src/views/component-viewer/dynamic-view-settings.ts b/src/views/component-viewer/dynamic-view-settings.ts new file mode 100644 index 00000000..c2322e48 --- /dev/null +++ b/src/views/component-viewer/dynamic-view-settings.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as vscode from 'vscode'; + +export interface DynamicViewState { + periodicUpdateEnabled?: boolean; + filterPattern?: string; +} + +export type DynamicViewStateByConfig = Record; + +export function readDynamicViewState(settingsKey: string, configStateKey: string): DynamicViewState | undefined { + const inspection = vscode.workspace.getConfiguration().inspect(settingsKey); + const globalState = inspection?.globalValue?.[configStateKey]; + const workspaceState = inspection?.workspaceValue?.[configStateKey]; + if (globalState === undefined && workspaceState === undefined) { + return undefined; + } + // User state provides defaults; Workspace state overrides only the properties it defines. + return { ...globalState, ...workspaceState, }; +} + +export async function writeDynamicViewState(settingsKey: string, configStateKey: string, state: DynamicViewState): Promise { + const statesToStore = { ...(vscode.workspace.getConfiguration().get(settingsKey) ?? {}) }; + if (Object.keys(state).length === 0) { + delete statesToStore[configStateKey]; + } else { + statesToStore[configStateKey] = state; + } + const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; + await vscode.workspace.getConfiguration().update(settingsKey, valueToStore, vscode.ConfigurationTarget.Workspace); +} + +export async function clearAllDynamicViewState(settingsKeys: string[]): Promise { + await Promise.all(settingsKeys.flatMap(key => [ + vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Workspace), + vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Global), + ])); +} From 6a929ef5e98e2da0e562749ae3df4c1302ef6e1b Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Mon, 11 May 2026 16:20:16 +0200 Subject: [PATCH 03/17] cpuStates: small improvement --- src/features/cpu-states/cpu-states.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 1d67810a..84133362 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -351,8 +351,18 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = true; - const statesToStore = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; - delete statesToStore[cpuStates.configStateKey]; + + const inspection = vscode.workspace.getConfiguration().inspect>(CpuStates.SETTINGS_KEY); + const userState = inspection?.globalValue?.[cpuStates.configStateKey]; + const statesToStore = { ...(inspection?.workspaceValue ?? {}) }; + // If User settings disable CPU states but this Workspace/session enables them, + // write true explicitly so the User value does not bleed through. + if (userState === false) { + statesToStore[cpuStates.configStateKey] = true; + } else { + delete statesToStore[cpuStates.configStateKey]; + } + const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, valueToStore, vscode.ConfigurationTarget.Workspace); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); From d09b874c01e890bb575e87af58413ecb05852110 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Mon, 11 May 2026 17:05:12 +0200 Subject: [PATCH 04/17] Buf fix about handling user/workspace settings --- src/features/cpu-states/cpu-states.ts | 9 ++++++--- src/views/component-viewer/dynamic-view-settings.ts | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 84133362..37761cfe 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -115,9 +115,11 @@ export class CpuStates { const cbuildRun = await session.getCbuildRun(); const targetType = cbuildRun?.getTargetType(); const configStateKey = targetType ? `${targetType}::${session.session.configuration.name}` : session.session.configuration.name; - const storedStates = vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}; + const inspection = vscode.workspace.getConfiguration().inspect>(CpuStates.SETTINGS_KEY); + // Merge 'User' and 'Workspace' levels — workspace takes precedence. + const mergedStates = { ...(inspection?.globalValue ?? {}), ...(inspection?.workspaceValue ?? {}) }; cpuStates.configStateKey = configStateKey; - cpuStates.enableCpuStatesFlag = storedStates[configStateKey] ?? true; + cpuStates.enableCpuStatesFlag = mergedStates[configStateKey] ?? true; // Following call might fail if target not stopped on connect, returns undefined // Retry on first Stopped Event. cpuStates.hasStates = await this.supportsCpuStates(session); @@ -374,7 +376,8 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = false; - const storedStates = { ...(vscode.workspace.getConfiguration().get>(CpuStates.SETTINGS_KEY) ?? {}) }; + const inspection = vscode.workspace.getConfiguration().inspect>(CpuStates.SETTINGS_KEY); + const storedStates = { ...(inspection?.workspaceValue ?? {}) }; storedStates[cpuStates.configStateKey] = false; await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, storedStates, vscode.ConfigurationTarget.Workspace); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); diff --git a/src/views/component-viewer/dynamic-view-settings.ts b/src/views/component-viewer/dynamic-view-settings.ts index c2322e48..f3fd0321 100644 --- a/src/views/component-viewer/dynamic-view-settings.ts +++ b/src/views/component-viewer/dynamic-view-settings.ts @@ -30,12 +30,13 @@ export function readDynamicViewState(settingsKey: string, configStateKey: string if (globalState === undefined && workspaceState === undefined) { return undefined; } - // User state provides defaults; Workspace state overrides only the properties it defines. + // 'User' state provides defaults; 'Workspace' state overrides only the properties it defines. return { ...globalState, ...workspaceState, }; } export async function writeDynamicViewState(settingsKey: string, configStateKey: string, state: DynamicViewState): Promise { - const statesToStore = { ...(vscode.workspace.getConfiguration().get(settingsKey) ?? {}) }; + const inspection = vscode.workspace.getConfiguration().inspect(settingsKey); + const statesToStore = { ...(inspection?.workspaceValue ?? {}) }; if (Object.keys(state).length === 0) { delete statesToStore[configStateKey]; } else { From c6c40af5ae8482aeab1f696879cda7660e108b43 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Mon, 11 May 2026 17:05:53 +0200 Subject: [PATCH 05/17] Fix existing unit tests --- __mocks__/vscode.js | 9 +++++++++ src/debug-session/__test__/debug-session.factory.ts | 9 +++++---- src/features/cpu-states/cpu-states.test.ts | 4 ++-- .../test/unit/component-viewer-base.test.ts | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/__mocks__/vscode.js b/__mocks__/vscode.js index bf4fb789..e908b326 100644 --- a/__mocks__/vscode.js +++ b/__mocks__/vscode.js @@ -30,6 +30,12 @@ const StatusBarAlignment = { Right: 2 }; +const ConfigurationTarget = { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, +}; + const MockTreeItemCollapsibleState = { None: 0, Collapsed: 1, @@ -117,6 +123,8 @@ module.exports = { workspace: { getConfiguration: jest.fn(() => ({ get: jest.fn(), + update: jest.fn().mockResolvedValue(undefined), + inspect: jest.fn().mockReturnValue(undefined), })), fs: { readFile: jest.fn(uri => { @@ -162,4 +170,5 @@ module.exports = { }, EnvironmentVariableMutatorType, StatusBarAlignment, + ConfigurationTarget, }; diff --git a/src/debug-session/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts index 7b5065f4..906e52ce 100644 --- a/src/debug-session/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -20,8 +20,8 @@ import { GDBTargetDebugSession, GDBTargetDebugTracker, TargetState } from '..'; export type OnRefreshCallback = (session: Session) => void; export type Session = { - session: { id: string }; - getCbuildRun: () => Promise<{ getScvdFilePaths: () => string[] } | undefined>; + session: { id: string; configuration?: { name: string } }; + getCbuildRun: () => Promise<{ getScvdFilePaths: () => string[]; getTargetType: () => string | undefined } | undefined>; getPname: () => Promise; refreshTimer: { onRefresh: (cb: OnRefreshCallback) => void }; targetState?: TargetState; @@ -88,10 +88,11 @@ export const debugSessionFactory = ( // Ensure same object returned for multiple calls to getCbuildRun. const cbuildRunMock = hasCbuildRun ? { getContents: jest.fn(), - getScvdFilePaths: () => paths + getScvdFilePaths: () => paths, + getTargetType: jest.fn(() => undefined), } : undefined; return { - session: { id }, + session: { id, configuration: { name: id } }, getCbuildRun: async () => cbuildRunMock, getPname: async () => pname, refreshTimer: { diff --git a/src/features/cpu-states/cpu-states.test.ts b/src/features/cpu-states/cpu-states.test.ts index 81834d79..b4a7b18e 100644 --- a/src/features/cpu-states/cpu-states.test.ts +++ b/src/features/cpu-states/cpu-states.test.ts @@ -443,13 +443,13 @@ describe('CpuStates', () => { it('enable cpu states sets enableCpuStates flag to true', async () => { cpuStates.activate(tracker); await cpuStates.enableCpuStates(); - expect((cpuStates as unknown as { enableCpuStatesFlag: boolean }).enableCpuStatesFlag).toEqual(true); + expect(cpuStates.activeCpuStates?.enableCpuStatesFlag).toEqual(true); }); it('disable cpu states sets enableCpuStates flag to false', async () => { cpuStates.activate(tracker); await cpuStates.disableCpuStates(); - expect((cpuStates as unknown as { enableCpuStatesFlag: boolean }).enableCpuStatesFlag).toEqual(false); + expect(cpuStates.activeCpuStates?.enableCpuStatesFlag).toEqual(false); }); }); diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index 7b4b074a..24cdb897 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -303,7 +303,7 @@ describe('ComponentViewerBase', () => { await controller.activate(tracker as unknown as GDBTargetDebugTracker); const session: Session = { - session: { id: 's1' }, + session: { id: 's1', configuration: { name: 's1' } }, getCbuildRun: async () => undefined, getPname: async () => undefined, refreshTimer: { onRefresh: jest.fn() }, From 4731a925c3eb423e130f2a405b06a75f628ad568 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Tue, 12 May 2026 11:06:18 +0200 Subject: [PATCH 06/17] Add unit tests --- .../__test__/debug-session.factory.ts | 2 +- src/features/cpu-states/cpu-states.test.ts | 115 +++++++++++ .../test/unit/component-viewer-base.test.ts | 178 ++++++++++++++++++ .../test/unit/dynamic-view-settings.test.ts | 107 +++++++++++ 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 src/views/component-viewer/test/unit/dynamic-view-settings.test.ts diff --git a/src/debug-session/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts index 906e52ce..06c42ba3 100644 --- a/src/debug-session/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -93,7 +93,7 @@ export const debugSessionFactory = ( } : undefined; return { session: { id, configuration: { name: id } }, - getCbuildRun: async () => cbuildRunMock, + getCbuildRun: jest.fn().mockResolvedValue(cbuildRunMock), getPname: async () => pname, refreshTimer: { onRefresh: jest.fn(), diff --git a/src/features/cpu-states/cpu-states.test.ts b/src/features/cpu-states/cpu-states.test.ts index b4a7b18e..d9be619b 100644 --- a/src/features/cpu-states/cpu-states.test.ts +++ b/src/features/cpu-states/cpu-states.test.ts @@ -488,5 +488,120 @@ describe('CpuStates', () => { }); + describe('CPU timer enable/disable state persists to and restores from settings', () => { + beforeEach(() => { + jest.spyOn(gdbtargetDebugSession, 'getCbuildRun').mockResolvedValue({ + getTargetType: () => 'My-Target', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('enabling CPU timer writes true to workspace to override user-level disabled setting', async () => { + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ + globalValue: { 'My-Target::Debug': false }, + workspaceValue: {}, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + cpuStates.activate(tracker); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onWillStartSession.fire(gdbtargetDebugSession); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onConnected.fire(gdbtargetDebugSession); + await waitForMs(0); + + // On connect the flag is false because user settings say false and workspace has no override. + expect(cpuStates.activeCpuStates?.enableCpuStatesFlag).toEqual(false); + // Workspace settings contain the explicit true so the user-level false no longer bleeds through. + await cpuStates.enableCpuStates(); + expect(cpuStates.activeCpuStates?.enableCpuStatesFlag).toEqual(true); + expect(updateMock).toHaveBeenCalledWith( + 'vscode-cmsis-debugger.cpuStates.viewState', { 'My-Target::Debug': true }, vscode.ConfigurationTarget.Workspace + ); + }); + + it('toolbar button state switches when changing the active debug session', async () => { + const executeCommandSpy = jest.spyOn(vscode.commands, 'executeCommand').mockResolvedValue(undefined); + const debugConfig2 = gdbTargetConfiguration({ name: 'Debug2' }); + const debugSession2 = debugSessionFactory(debugConfig2, '{session-id-2}'); + const gdbtargetDebugSession2 = new GDBTargetDebugSession(debugSession2); + cpuStates.activate(tracker); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onWillStartSession.fire(gdbtargetDebugSession); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onWillStartSession.fire(gdbtargetDebugSession2); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (cpuStates as any).sessionCpuStates.get(gdbtargetDebugSession2.session.id)!.enableCpuStatesFlag = false; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession); + expect(executeCommandSpy).toHaveBeenCalledWith('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); + executeCommandSpy.mockClear(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession2); + expect(executeCommandSpy).toHaveBeenCalledWith('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', false); + executeCommandSpy.mockClear(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onDidChangeActiveDebugSession.fire(undefined); + expect(executeCommandSpy).toHaveBeenCalledWith('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', false); + }); + + it('disabling CPU timer only saves to workspace and does not pull in user-level keys', async () => { + const otherKey = 'OtherProject::OtherConfig'; + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ + globalValue: { [otherKey]: false }, + workspaceValue: {}, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + cpuStates.activate(tracker); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onWillStartSession.fire(gdbtargetDebugSession); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession); + await cpuStates.disableCpuStates(); + + const writtenValue = updateMock.mock.calls[0]?.[1] as Record; + expect(writtenValue).not.toHaveProperty(otherKey); + expect(writtenValue).toHaveProperty(cpuStates.activeCpuStates!.configStateKey, false); + }); + + it('clears both workspace and user settings, re-enables the session, and sets context key', async () => { + const updateMock = jest.fn().mockResolvedValue(undefined); + const executeCommandSpy = jest.spyOn(vscode.commands, 'executeCommand').mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + cpuStates.activate(tracker); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tracker as any)._onWillStartSession.fire(gdbtargetDebugSession); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (cpuStates as any).sessionCpuStates.get(gdbtargetDebugSession.session.id)!.enableCpuStatesFlag = false; + await cpuStates.resetViewState(); + + expect(updateMock).toHaveBeenCalledWith('vscode-cmsis-debugger.cpuStates.viewState', undefined, vscode.ConfigurationTarget.Workspace); + expect(updateMock).toHaveBeenCalledWith('vscode-cmsis-debugger.cpuStates.viewState', undefined, vscode.ConfigurationTarget.Global); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((cpuStates as any).sessionCpuStates.get(gdbtargetDebugSession.session.id)!.enableCpuStatesFlag).toBe(true); + expect(executeCommandSpy).toHaveBeenCalledWith('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); + }); + + }); }); diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index 24cdb897..f04b3555 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -1004,4 +1004,182 @@ describe('ComponentViewerBase', () => { await expect(handleExpandAll()).resolves.toBeUndefined(); expect(provider.expandAllElements).not.toHaveBeenCalled(); }); + + describe('view state save and restore', () => { + const SETTINGS_KEY = 'vscode-cmsis-debugger.testClass.viewState'; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('saveCurrentState writes active filter pattern to workspace settings', async () => { + const session = debugSessionFactory('s1'); + (controller as unknown as { _activeSession?: Session })._activeSession = session; + Object.defineProperty(provider, 'filterPattern', { get: () => 'word', configurable: true }); + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); + await saveCurrentState(); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { s1: { filterPattern: 'word' } }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('saveCurrentState writes periodicUpdateEnabled=false when auto-refresh is disabled', async () => { + const session = debugSessionFactory('s1'); + (controller as unknown as { _activeSession?: Session })._activeSession = session; + (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); + await saveCurrentState(); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { s1: { periodicUpdateEnabled: false } }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('saveCurrentState writes explicit true to workspace when user setting is false (prevents user-level bleed-through)', async () => { + const session = debugSessionFactory('s1'); + (controller as unknown as { _activeSession?: Session })._activeSession = session; + (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = true; + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ + globalValue: { s1: { periodicUpdateEnabled: false } }, + workspaceValue: {}, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); + await saveCurrentState(); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { s1: { periodicUpdateEnabled: true } }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('saveCurrentState uses target-type prefix in the configStateKey', async () => { + const session = debugSessionFactory('s1'); + (session.getCbuildRun as jest.Mock).mockResolvedValue({ + getTargetType: () => 'My-Target', + getScvdFilePaths: () => [], + }); + (controller as unknown as { _activeSession?: Session })._activeSession = session; + (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); + await saveCurrentState(); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { 'My-Target::s1': { periodicUpdateEnabled: false } }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('restorePeriodicUpdateAndFilter resets to defaults before applying saved state (prevents session state leaking)', async () => { + (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const session = debugSessionFactory('s1'); + const restoreState = (controller as unknown as { + restorePeriodicUpdateAndFilter: (s: Session) => Promise; + }).restorePeriodicUpdateAndFilter.bind(controller); + await restoreState(session); + + // Default (true) must be applied even though there are no saved settings. + expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(true); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', true); + expect(provider.setFilter).toHaveBeenCalledWith(undefined); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.filterActive', false); + }); + + it('restorePeriodicUpdateAndFilter restores periodicUpdateEnabled and filter from workspace settings', async () => { + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + inspect: jest.fn().mockReturnValue({ + globalValue: {}, + workspaceValue: { s1: { periodicUpdateEnabled: false, filterPattern: 'uart' } }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const session = debugSessionFactory('s1'); + const restoreState = (controller as unknown as { + restorePeriodicUpdateAndFilter: (s: Session) => Promise; + }).restorePeriodicUpdateAndFilter.bind(controller); + await restoreState(session); + + expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(false); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', false); + expect(provider.setFilter).toHaveBeenCalledWith('uart'); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.filterActive', true); + }); + + it('restorePeriodicUpdateAndFilter falls back to user settings when workspace has no entry', async () => { + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + inspect: jest.fn().mockReturnValue({ + globalValue: { s1: { periodicUpdateEnabled: false } }, + workspaceValue: {}, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const session = debugSessionFactory('s1'); + const restoreState = (controller as unknown as { + restorePeriodicUpdateAndFilter: (s: Session) => Promise; + }).restorePeriodicUpdateAndFilter.bind(controller); + await restoreState(session); + + expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(false); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', false); + }); + + it('resetViewState clears persisted settings, resets in-memory state, and unlocks all instances', async () => { + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; + const root = makeGuiNode('root'); + const inst = instanceFactory(); + inst.getGuiTree = jest.fn(() => [root]); + (controller as unknown as { _instances: unknown[] })._instances = [ + { componentViewerInstance: inst, lockState: true, sessionId: 's1', dirtyWhileLocked: false }, + ]; + await controller.resetViewState(); + + expect(updateMock).toHaveBeenCalledWith(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace); + expect(updateMock).toHaveBeenCalledWith(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Global); + expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(true); + expect(provider.setFilter).toHaveBeenCalledWith(undefined); + expect((controller as unknown as { _instances: Array<{ lockState: boolean }> })._instances[0].lockState).toBe(false); + expect(root.isLocked).toBe(false); + }); + }); }); diff --git a/src/views/component-viewer/test/unit/dynamic-view-settings.test.ts b/src/views/component-viewer/test/unit/dynamic-view-settings.test.ts new file mode 100644 index 00000000..815b5864 --- /dev/null +++ b/src/views/component-viewer/test/unit/dynamic-view-settings.test.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as vscode from 'vscode'; +import { readDynamicViewState, writeDynamicViewState } from '../../dynamic-view-settings'; + +const SETTINGS_KEY = 'test.viewState'; +const CONFIG_KEY = 'My-Target::Debug'; + +describe('dynamic-view-settings', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('readDynamicViewState', () => { + it('returns user-level state when only the user level has an entry', () => { + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + inspect: jest.fn().mockReturnValue({ + globalValue: { [CONFIG_KEY]: { periodicUpdateEnabled: false } }, + workspaceValue: {}, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + expect(readDynamicViewState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ periodicUpdateEnabled: false }); + }); + + it('merges both levels with workspace taking precedence over user', () => { + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + inspect: jest.fn().mockReturnValue({ + globalValue: { [CONFIG_KEY]: { periodicUpdateEnabled: false, filterPattern: 'user-filter' } }, + workspaceValue: { [CONFIG_KEY]: { periodicUpdateEnabled: true } }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + expect(readDynamicViewState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ + periodicUpdateEnabled: true, + filterPattern: 'user-filter', + }); + }); + }); + + describe('writeDynamicViewState', () => { + it('writes only to workspaceValue and does not pull in user-level keys', async () => { + const updateMock = jest.fn().mockResolvedValue(undefined); + const foreignKey = 'OtherProject::OtherConfig'; + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ + globalValue: { [foreignKey]: { periodicUpdateEnabled: true } }, + workspaceValue: {}, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + await writeDynamicViewState(SETTINGS_KEY, CONFIG_KEY, { periodicUpdateEnabled: false }); + + const written = updateMock.mock.calls[0]?.[1] as Record; + expect(written).not.toHaveProperty(foreignKey); + expect(written).toHaveProperty(CONFIG_KEY, { periodicUpdateEnabled: false }); + }); + + it('preserves other configuration keys when writing a new entry', async () => { + const updateMock = jest.fn().mockResolvedValue(undefined); + const existingKey = 'OtherTarget::OtherConfig'; + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ + globalValue: {}, + workspaceValue: { [existingKey]: { periodicUpdateEnabled: false } }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + await writeDynamicViewState(SETTINGS_KEY, CONFIG_KEY, { filterPattern: 'word' }); + + const written = updateMock.mock.calls[0]?.[1] as Record; + expect(written).toHaveProperty(existingKey); + expect(written).toHaveProperty(CONFIG_KEY, { filterPattern: 'word' }); + }); + + it('removes the key when state is empty and no keys remain', async () => { + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ + globalValue: {}, + workspaceValue: { [CONFIG_KEY]: { periodicUpdateEnabled: false } }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + await writeDynamicViewState(SETTINGS_KEY, CONFIG_KEY, {}); + + expect(updateMock).toHaveBeenCalledWith(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace); + }); + }); +}); From 796485083e2d8c317a6628fd5f0e547f05b43e46 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Tue, 12 May 2026 15:23:30 +0200 Subject: [PATCH 07/17] Refactor code --- .../__test__/debug-session.factory.ts | 2 + src/debug-session/gdbtarget-debug-session.ts | 7 + src/features/cpu-states/cpu-states.ts | 37 +-- .../component-viewer/component-viewer-base.ts | 28 +- .../component-viewer/dynamic-view-settings.ts | 54 ---- .../test/unit/component-viewer-base.test.ts | 7 +- .../test/unit/dynamic-view-settings.test.ts | 107 ------- src/views/dynamic-view-states.test.ts | 287 ++++++++++++++++++ src/views/dynamic-view-states.ts | 110 +++++++ 9 files changed, 422 insertions(+), 217 deletions(-) delete mode 100644 src/views/component-viewer/dynamic-view-settings.ts delete mode 100644 src/views/component-viewer/test/unit/dynamic-view-settings.test.ts create mode 100644 src/views/dynamic-view-states.test.ts create mode 100644 src/views/dynamic-view-states.ts diff --git a/src/debug-session/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts index 06c42ba3..2a418e7c 100644 --- a/src/debug-session/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -22,6 +22,7 @@ export type OnRefreshCallback = (session: Session) => void; export type Session = { session: { id: string; configuration?: { name: string } }; getCbuildRun: () => Promise<{ getScvdFilePaths: () => string[]; getTargetType: () => string | undefined } | undefined>; + getConfigStateKey: () => Promise; getPname: () => Promise; refreshTimer: { onRefresh: (cb: OnRefreshCallback) => void }; targetState?: TargetState; @@ -94,6 +95,7 @@ export const debugSessionFactory = ( return { session: { id, configuration: { name: id } }, getCbuildRun: jest.fn().mockResolvedValue(cbuildRunMock), + getConfigStateKey: jest.fn().mockResolvedValue(id), getPname: async () => pname, refreshTimer: { onRefresh: jest.fn(), diff --git a/src/debug-session/gdbtarget-debug-session.ts b/src/debug-session/gdbtarget-debug-session.ts index 3934017b..4d256ab5 100644 --- a/src/debug-session/gdbtarget-debug-session.ts +++ b/src/debug-session/gdbtarget-debug-session.ts @@ -109,6 +109,13 @@ export class GDBTargetDebugSession { return pname; } + public async getConfigStateKey(): Promise { + const cbuildRun = await this.getCbuildRun(); + const targetType = cbuildRun?.getTargetType(); + const configStateKey = targetType ? `${targetType}::${this.session.configuration.name}` : this.session.configuration.name; + return configStateKey; + } + /** * Check if first stop attempt for session is done by 'terminate' request. * Notes: diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 37761cfe..a27332cf 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -27,6 +27,7 @@ import { CpuStatesHistory } from './cpu-states-history'; import { calculateTime, extractPname } from '../../utils'; import { GDBTargetConfiguration } from '../../debug-configuration'; import { logger } from '../../logger'; +import {clearAllCpuStatesState, readCpuStatesEnabled, writeCpuStatesEnabled} from '../../views/dynamic-view-states'; // Architecturally defined registers (M-profile) const DWT_CTRL_ADDRESS = 0xE0001000; @@ -111,15 +112,10 @@ export class CpuStates { if (!cpuStates) { return; } - // cbuild-run is now parsed. Refine the key with the target-type prefix and restore the enabled/disabled state from settings.json - const cbuildRun = await session.getCbuildRun(); - const targetType = cbuildRun?.getTargetType(); - const configStateKey = targetType ? `${targetType}::${session.session.configuration.name}` : session.session.configuration.name; - const inspection = vscode.workspace.getConfiguration().inspect>(CpuStates.SETTINGS_KEY); - // Merge 'User' and 'Workspace' levels — workspace takes precedence. - const mergedStates = { ...(inspection?.globalValue ?? {}), ...(inspection?.workspaceValue ?? {}) }; + // Refine the key with the target-type prefix and restore the enabled/disabled state from settings.json + const configStateKey = await session.getConfigStateKey(); cpuStates.configStateKey = configStateKey; - cpuStates.enableCpuStatesFlag = mergedStates[configStateKey] ?? true; + cpuStates.enableCpuStatesFlag = readCpuStatesEnabled(CpuStates.SETTINGS_KEY, configStateKey) ?? true; // Following call might fail if target not stopped on connect, returns undefined // Retry on first Stopped Event. cpuStates.hasStates = await this.supportsCpuStates(session); @@ -353,20 +349,7 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = true; - - const inspection = vscode.workspace.getConfiguration().inspect>(CpuStates.SETTINGS_KEY); - const userState = inspection?.globalValue?.[cpuStates.configStateKey]; - const statesToStore = { ...(inspection?.workspaceValue ?? {}) }; - // If User settings disable CPU states but this Workspace/session enables them, - // write true explicitly so the User value does not bleed through. - if (userState === false) { - statesToStore[cpuStates.configStateKey] = true; - } else { - delete statesToStore[cpuStates.configStateKey]; - } - - const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; - await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, valueToStore, vscode.ConfigurationTarget.Workspace); + await writeCpuStatesEnabled(CpuStates.SETTINGS_KEY, cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } @@ -376,19 +359,13 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = false; - const inspection = vscode.workspace.getConfiguration().inspect>(CpuStates.SETTINGS_KEY); - const storedStates = { ...(inspection?.workspaceValue ?? {}) }; - storedStates[cpuStates.configStateKey] = false; - await vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, storedStates, vscode.ConfigurationTarget.Workspace); + await writeCpuStatesEnabled(CpuStates.SETTINGS_KEY, cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } public async resetViewState(): Promise { // Clear all persisted cpu states settings from both workspace/user settings.json - await Promise.all([ - vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace), - vscode.workspace.getConfiguration().update(CpuStates.SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Global), - ]); + await clearAllCpuStatesState([CpuStates.SETTINGS_KEY]); // Re-enable all active sessions in memory for (const states of this.sessionCpuStates.values()) { states.enableCpuStatesFlag = true; diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index e522d994..6019da76 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -25,7 +25,7 @@ import { perf, parsePerf } from './stats-config'; import { vscodeViewExists } from '../../vscode-utils'; import { EXTENSION_NAME, VIEW_PREFIX } from '../../manifest'; import { ExtendedGDBTargetConfiguration } from '../../debug-configuration/gdbtarget-configuration'; -import { readDynamicViewState, writeDynamicViewState, clearAllDynamicViewState, DynamicViewState, DynamicViewStateByConfig } from './dynamic-view-settings'; +import { readComponentViewerState, writeComponentViewerState, clearAllComponentViewerState } from '../dynamic-view-states'; export interface ScvdCollector { getScvdFilePaths(session: GDBTargetDebugSession): Promise; @@ -534,39 +534,23 @@ export class ComponentViewerBase { return `${EXTENSION_NAME}.${this._viewId}.viewState`; } - private async sessionStateKey(session: GDBTargetDebugSession): Promise { - const cbuildRun = await session.getCbuildRun(); - const targetType = cbuildRun?.getTargetType(); - const configName = session.session.configuration.name; - return targetType ? `${targetType}::${configName}` : configName; - } - private async saveCurrentState(): Promise { if (!this._activeSession) { return; } - const configStateKey = await this.sessionStateKey(this._activeSession); + const configStateKey = await this._activeSession.getConfigStateKey(); const filterPattern = this._componentViewerTreeDataProvider.filterPattern; - // If User settings disable periodicUpdate update but this Workspace/session enables it, - // write true explicitly so the User value does not bleed through. - const inspection = vscode.workspace.getConfiguration().inspect(this._settingsKey); - const userState = inspection?.globalValue?.[configStateKey]; - const needsExplicitPeriodicUpdate = this._refreshTimerEnabled && userState?.periodicUpdateEnabled === false; - const state: DynamicViewState = { - ...(!this._refreshTimerEnabled || needsExplicitPeriodicUpdate ? { periodicUpdateEnabled: this._refreshTimerEnabled } : {}), - ...(filterPattern !== undefined ? { filterPattern } : {}), - }; - await writeDynamicViewState(this._settingsKey, configStateKey, state); + await writeComponentViewerState( this._settingsKey, configStateKey, this._refreshTimerEnabled, filterPattern); } private async restorePeriodicUpdateAndFilter(session: GDBTargetDebugSession): Promise { - // Always reset to defaults before applying saved state to prevent state leaking between sessions + // Always reset to defaults before applying saved state to prevent state leaking while switching sessions this._refreshTimerEnabled = true; vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); this._componentViewerTreeDataProvider.setFilter(undefined); vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, false); - const state = readDynamicViewState(this._settingsKey, await this.sessionStateKey(session)); + const state = readComponentViewerState(this._settingsKey, await session.getConfigStateKey()); if (!state) { return; } @@ -584,7 +568,7 @@ export class ComponentViewerBase { public async resetViewState(): Promise { // Clear persisted settings - await clearAllDynamicViewState([this._settingsKey]); + await clearAllComponentViewerState([this._settingsKey]); // Reset in-memory state to defaults this._refreshTimerEnabled = true; vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); diff --git a/src/views/component-viewer/dynamic-view-settings.ts b/src/views/component-viewer/dynamic-view-settings.ts deleted file mode 100644 index f3fd0321..00000000 --- a/src/views/component-viewer/dynamic-view-settings.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2026 Arm Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as vscode from 'vscode'; - -export interface DynamicViewState { - periodicUpdateEnabled?: boolean; - filterPattern?: string; -} - -export type DynamicViewStateByConfig = Record; - -export function readDynamicViewState(settingsKey: string, configStateKey: string): DynamicViewState | undefined { - const inspection = vscode.workspace.getConfiguration().inspect(settingsKey); - const globalState = inspection?.globalValue?.[configStateKey]; - const workspaceState = inspection?.workspaceValue?.[configStateKey]; - if (globalState === undefined && workspaceState === undefined) { - return undefined; - } - // 'User' state provides defaults; 'Workspace' state overrides only the properties it defines. - return { ...globalState, ...workspaceState, }; -} - -export async function writeDynamicViewState(settingsKey: string, configStateKey: string, state: DynamicViewState): Promise { - const inspection = vscode.workspace.getConfiguration().inspect(settingsKey); - const statesToStore = { ...(inspection?.workspaceValue ?? {}) }; - if (Object.keys(state).length === 0) { - delete statesToStore[configStateKey]; - } else { - statesToStore[configStateKey] = state; - } - const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; - await vscode.workspace.getConfiguration().update(settingsKey, valueToStore, vscode.ConfigurationTarget.Workspace); -} - -export async function clearAllDynamicViewState(settingsKeys: string[]): Promise { - await Promise.all(settingsKeys.flatMap(key => [ - vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Workspace), - vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Global), - ])); -} diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index f04b3555..014f64fd 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -183,6 +183,7 @@ describe('ComponentViewerBase', () => { const sessionNoReader: Session = { session: { id: 's1' }, getCbuildRun: async () => undefined, + getConfigStateKey: async () => 's1', getPname: async () => undefined, refreshTimer: { onRefresh: jest.fn() }, }; @@ -305,6 +306,7 @@ describe('ComponentViewerBase', () => { const session: Session = { session: { id: 's1', configuration: { name: 's1' } }, getCbuildRun: async () => undefined, + getConfigStateKey: async () => 's1', getPname: async () => undefined, refreshTimer: { onRefresh: jest.fn() }, }; @@ -1077,10 +1079,7 @@ describe('ComponentViewerBase', () => { it('saveCurrentState uses target-type prefix in the configStateKey', async () => { const session = debugSessionFactory('s1'); - (session.getCbuildRun as jest.Mock).mockResolvedValue({ - getTargetType: () => 'My-Target', - getScvdFilePaths: () => [], - }); + (session.getConfigStateKey as jest.Mock).mockResolvedValue('My-Target::s1'); (controller as unknown as { _activeSession?: Session })._activeSession = session; (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; const updateMock = jest.fn().mockResolvedValue(undefined); diff --git a/src/views/component-viewer/test/unit/dynamic-view-settings.test.ts b/src/views/component-viewer/test/unit/dynamic-view-settings.test.ts deleted file mode 100644 index 815b5864..00000000 --- a/src/views/component-viewer/test/unit/dynamic-view-settings.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright 2026 Arm Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as vscode from 'vscode'; -import { readDynamicViewState, writeDynamicViewState } from '../../dynamic-view-settings'; - -const SETTINGS_KEY = 'test.viewState'; -const CONFIG_KEY = 'My-Target::Debug'; - -describe('dynamic-view-settings', () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('readDynamicViewState', () => { - it('returns user-level state when only the user level has an entry', () => { - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - inspect: jest.fn().mockReturnValue({ - globalValue: { [CONFIG_KEY]: { periodicUpdateEnabled: false } }, - workspaceValue: {}, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - expect(readDynamicViewState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ periodicUpdateEnabled: false }); - }); - - it('merges both levels with workspace taking precedence over user', () => { - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - inspect: jest.fn().mockReturnValue({ - globalValue: { [CONFIG_KEY]: { periodicUpdateEnabled: false, filterPattern: 'user-filter' } }, - workspaceValue: { [CONFIG_KEY]: { periodicUpdateEnabled: true } }, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - expect(readDynamicViewState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ - periodicUpdateEnabled: true, - filterPattern: 'user-filter', - }); - }); - }); - - describe('writeDynamicViewState', () => { - it('writes only to workspaceValue and does not pull in user-level keys', async () => { - const updateMock = jest.fn().mockResolvedValue(undefined); - const foreignKey = 'OtherProject::OtherConfig'; - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ - globalValue: { [foreignKey]: { periodicUpdateEnabled: true } }, - workspaceValue: {}, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - await writeDynamicViewState(SETTINGS_KEY, CONFIG_KEY, { periodicUpdateEnabled: false }); - - const written = updateMock.mock.calls[0]?.[1] as Record; - expect(written).not.toHaveProperty(foreignKey); - expect(written).toHaveProperty(CONFIG_KEY, { periodicUpdateEnabled: false }); - }); - - it('preserves other configuration keys when writing a new entry', async () => { - const updateMock = jest.fn().mockResolvedValue(undefined); - const existingKey = 'OtherTarget::OtherConfig'; - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ - globalValue: {}, - workspaceValue: { [existingKey]: { periodicUpdateEnabled: false } }, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - await writeDynamicViewState(SETTINGS_KEY, CONFIG_KEY, { filterPattern: 'word' }); - - const written = updateMock.mock.calls[0]?.[1] as Record; - expect(written).toHaveProperty(existingKey); - expect(written).toHaveProperty(CONFIG_KEY, { filterPattern: 'word' }); - }); - - it('removes the key when state is empty and no keys remain', async () => { - const updateMock = jest.fn().mockResolvedValue(undefined); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ - globalValue: {}, - workspaceValue: { [CONFIG_KEY]: { periodicUpdateEnabled: false } }, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - await writeDynamicViewState(SETTINGS_KEY, CONFIG_KEY, {}); - - expect(updateMock).toHaveBeenCalledWith(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace); - }); - }); -}); diff --git a/src/views/dynamic-view-states.test.ts b/src/views/dynamic-view-states.test.ts new file mode 100644 index 00000000..7cb9a5b7 --- /dev/null +++ b/src/views/dynamic-view-states.test.ts @@ -0,0 +1,287 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as vscode from 'vscode'; +import { + clearAllComponentViewerState, + readComponentViewerState, + readCpuStatesEnabled, + writeComponentViewerState, + writeCpuStatesEnabled, +} from './dynamic-view-states'; + +const SETTINGS_KEY = 'test.viewState'; +const CONFIG_KEY = 'My-Target::Debug'; + +function mockGetConfiguration(globalValue: Record = {}, workspaceValue: Record = {}): jest.Mock { + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + inspect: jest.fn().mockReturnValue({ globalValue, workspaceValue }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + return updateMock; +} + +describe('dynamic-view-states', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('Component Viewer state', () => { + it('returns user-level state when workspace is empty', () => { + mockGetConfiguration({ + [CONFIG_KEY]: { + periodicUpdateEnabled: false, + }, + }); + expect(readComponentViewerState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ + periodicUpdateEnabled: false, + }); + }); + + it('merges user and workspace state when reading', () => { + mockGetConfiguration( + { + [CONFIG_KEY]: { + periodicUpdateEnabled: false, + filterPattern: 'user-filter', + }, + }, { + [CONFIG_KEY]: { + periodicUpdateEnabled: true, + }, + } + ); + expect(readComponentViewerState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ + periodicUpdateEnabled: true, + filterPattern: 'user-filter', + }); + }); + + it('writes disabled periodic update state to workspace settings', async () => { + const updateMock = mockGetConfiguration(); + await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, false, undefined); + expect(updateMock).toHaveBeenCalledWith( SETTINGS_KEY, + { + [CONFIG_KEY]: { + periodicUpdateEnabled: false, + }, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('removes workspace state when periodic update is enabled and user setting does not conflict', async () => { + const updateMock = mockGetConfiguration(); + + await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('writes explicit enabled state when user setting disables periodic update', async () => { + const updateMock = mockGetConfiguration({ + [CONFIG_KEY]: { + periodicUpdateEnabled: false, + }, + }); + + await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { + [CONFIG_KEY]: { + periodicUpdateEnabled: true, + }, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('writes active filter pattern to workspace settings', async () => { + const updateMock = mockGetConfiguration(); + + await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, 'uart'); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { + [CONFIG_KEY]: { + filterPattern: 'uart', + }, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('writes filter pattern together with disabled periodic update state', async () => { + const updateMock = mockGetConfiguration(); + + await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, false, 'uart'); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { + [CONFIG_KEY]: { + periodicUpdateEnabled: false, + filterPattern: 'uart', + }, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('preserves other workspace entries when writing state', async () => { + const otherConfigKey = 'Other-Target::Debug'; + const updateMock = mockGetConfiguration( + {}, + { + [otherConfigKey]: { + periodicUpdateEnabled: false, + }, + } + ); + + await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, 'uart'); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { + [otherConfigKey]: { + periodicUpdateEnabled: false, + }, + [CONFIG_KEY]: { + filterPattern: 'uart', + }, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('clears both workspace and global levels', async () => { + const updateMock = jest.fn().mockResolvedValue(undefined); + jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ + update: updateMock, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + await clearAllComponentViewerState([SETTINGS_KEY]); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + undefined, + vscode.ConfigurationTarget.Workspace + ); + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + undefined, + vscode.ConfigurationTarget.Global + ); + }); + }); + + describe('CPU States settings', () => { + it('returns user-level value when workspace is empty', () => { + mockGetConfiguration({ + [CONFIG_KEY]: false, + }); + + expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(false); + }); + + it('uses workspace value before user value when reading', () => { + mockGetConfiguration( + { + [CONFIG_KEY]: false, + }, + { + [CONFIG_KEY]: true, + } + ); + + expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(true); + }); + + it('writes disabled CPU states value to workspace settings', async () => { + const updateMock = mockGetConfiguration(); + + await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, false); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { + [CONFIG_KEY]: false, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('removes workspace state when CPU states are enabled and user setting does not conflict', async () => { + const updateMock = mockGetConfiguration(); + + await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, true); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('writes explicit enabled value when user setting disables CPU states', async () => { + const updateMock = mockGetConfiguration({ + [CONFIG_KEY]: false, + }); + + await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, true); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { + [CONFIG_KEY]: true, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + + it('preserves other workspace entries when writing CPU states value', async () => { + const otherConfigKey = 'Other-Target::Debug'; + const updateMock = mockGetConfiguration( + {}, + { + [otherConfigKey]: false, + } + ); + + await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, false); + + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, + { + [otherConfigKey]: false, + [CONFIG_KEY]: false, + }, + vscode.ConfigurationTarget.Workspace + ); + }); + }); +}); \ No newline at end of file diff --git a/src/views/dynamic-view-states.ts b/src/views/dynamic-view-states.ts new file mode 100644 index 00000000..302f1ba9 --- /dev/null +++ b/src/views/dynamic-view-states.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as vscode from 'vscode'; + +type ConfigStateByKey = Record; + +function readConfigState(settingsKey: string, configStateKey: string): T | undefined { + const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); + const globalState = inspection?.globalValue?.[configStateKey]; + const workspaceState = inspection?.workspaceValue?.[configStateKey]; + return workspaceState ?? globalState; +} + +function readMergedConfigState(settingsKey: string, configStateKey: string): T | undefined { + const inspection = vscode.workspace.getConfiguration().inspect>>(settingsKey); + const globalState = inspection?.globalValue?.[configStateKey]; + const workspaceState = inspection?.workspaceValue?.[configStateKey]; + if (globalState === undefined && workspaceState === undefined) { + return undefined; + } + // 'User' state provides defaults; 'Workspace' state overrides only the properties it defines. + return { ...(globalState ?? {}), ...(workspaceState ?? {}) } as T; +} + +async function writeConfigState(settingsKey: string, configStateKey: string, state: T | undefined): Promise { + const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); + const statesToStore = { ...(inspection?.workspaceValue ?? {}) }; + if (state === undefined) { + delete statesToStore[configStateKey]; + } else { + statesToStore[configStateKey] = state; + } + const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; + await vscode.workspace.getConfiguration().update(settingsKey, valueToStore, vscode.ConfigurationTarget.Workspace); +} + +async function clearAllConfigState(settingsKeys: string[]): Promise { + await Promise.all(settingsKeys.flatMap(key => [ + vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Workspace), + vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Global), + ])); +} + +// ------------------------------------------------------------------------------------------------- +// Component Viewer settings +// ------------------------------------------------------------------------------------------------- + +export interface ComponentViewerState { + periodicUpdateEnabled?: boolean; + filterPattern?: string; +} + +type ComponentViewerStateByConfig = ConfigStateByKey; + +export function readComponentViewerState(settingsKey: string, configStateKey: string): ComponentViewerState | undefined { + return readMergedConfigState(settingsKey, configStateKey); +} + +export async function writeComponentViewerState(settingsKey: string, configStateKey: string, refreshTimerEnabled: boolean, filterPattern: string | undefined +): Promise { + const inspection = vscode.workspace.getConfiguration().inspect(settingsKey); + const userState = inspection?.globalValue?.[configStateKey]; + // If 'User' settings disable periodicUpdate but this 'Workspace' enables it, + // write true explicitly so the 'User' value does not bleed through. + const needsExplicitPeriodicUpdate = refreshTimerEnabled && userState?.periodicUpdateEnabled === false; + const state: ComponentViewerState = { + ...(!refreshTimerEnabled || needsExplicitPeriodicUpdate ? { periodicUpdateEnabled: refreshTimerEnabled } : {}), + ...(filterPattern !== undefined ? { filterPattern } : {}), + }; + await writeConfigState(settingsKey, configStateKey, Object.keys(state).length === 0 ? undefined : state); +} + +export async function clearAllComponentViewerState(settingsKeys: string[]): Promise { + await clearAllConfigState(settingsKeys); +} + +// ------------------------------------------------------------------------------------------------- +// CPU States settings +// ------------------------------------------------------------------------------------------------- + +export function readCpuStatesEnabled(settingsKey: string, configStateKey: string): boolean | undefined { + return readConfigState(settingsKey, configStateKey); +} + +export async function writeCpuStatesEnabled(settingsKey: string, configStateKey: string, enabled: boolean): Promise { + const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); + const userState = inspection?.globalValue?.[configStateKey]; + // If 'User' settings disable periodicUpdate but this 'Workspace' enables it, + // write true explicitly so the 'User' value does not bleed through. + const stateToStore = enabled ? userState === false ? true : undefined : false; + await writeConfigState(settingsKey, configStateKey, stateToStore); +} + +export async function clearAllCpuStatesState(settingsKeys: string[]): Promise { + await clearAllConfigState(settingsKeys); +} From aed60b7d5349b8034819899d154e588abf388106 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 13 May 2026 10:29:36 +0200 Subject: [PATCH 08/17] Small bug fix and refine unit tests --- src/features/cpu-states/cpu-states.test.ts | 47 +------ src/features/cpu-states/cpu-states.ts | 2 +- .../component-viewer/component-viewer-base.ts | 10 +- .../test/unit/component-viewer-base.test.ts | 123 +----------------- src/views/dynamic-view-states.test.ts | 103 +-------------- 5 files changed, 21 insertions(+), 264 deletions(-) diff --git a/src/features/cpu-states/cpu-states.test.ts b/src/features/cpu-states/cpu-states.test.ts index d9be619b..f7e7e81b 100644 --- a/src/features/cpu-states/cpu-states.test.ts +++ b/src/features/cpu-states/cpu-states.test.ts @@ -500,12 +500,12 @@ describe('CpuStates', () => { jest.restoreAllMocks(); }); - it('enabling CPU timer writes true to workspace to override user-level disabled setting', async () => { - const updateMock = jest.fn().mockResolvedValue(undefined); + it('restores CPU timer enabled state from settings on connect', async () => { jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, inspect: jest.fn().mockReturnValue({ - globalValue: { 'My-Target::Debug': false }, + globalValue: { + 'My-Target::Debug': false, + }, workspaceValue: {}, }), // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -519,14 +519,7 @@ describe('CpuStates', () => { (tracker as any)._onConnected.fire(gdbtargetDebugSession); await waitForMs(0); - // On connect the flag is false because user settings say false and workspace has no override. expect(cpuStates.activeCpuStates?.enableCpuStatesFlag).toEqual(false); - // Workspace settings contain the explicit true so the user-level false no longer bleeds through. - await cpuStates.enableCpuStates(); - expect(cpuStates.activeCpuStates?.enableCpuStatesFlag).toEqual(true); - expect(updateMock).toHaveBeenCalledWith( - 'vscode-cmsis-debugger.cpuStates.viewState', { 'My-Target::Debug': true }, vscode.ConfigurationTarget.Workspace - ); }); it('toolbar button state switches when changing the active debug session', async () => { @@ -557,37 +550,13 @@ describe('CpuStates', () => { expect(executeCommandSpy).toHaveBeenCalledWith('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', false); }); - it('disabling CPU timer only saves to workspace and does not pull in user-level keys', async () => { - const otherKey = 'OtherProject::OtherConfig'; - const updateMock = jest.fn().mockResolvedValue(undefined); + it('re-enables sessions and updates the toolbar context', async () => { jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ - globalValue: { [otherKey]: false }, - workspaceValue: {}, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - cpuStates.activate(tracker); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (tracker as any)._onWillStartSession.fire(gdbtargetDebugSession); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (tracker as any)._onDidChangeActiveDebugSession.fire(gdbtargetDebugSession); - await cpuStates.disableCpuStates(); - - const writtenValue = updateMock.mock.calls[0]?.[1] as Record; - expect(writtenValue).not.toHaveProperty(otherKey); - expect(writtenValue).toHaveProperty(cpuStates.activeCpuStates!.configStateKey, false); - }); - - it('clears both workspace and user settings, re-enables the session, and sets context key', async () => { - const updateMock = jest.fn().mockResolvedValue(undefined); - const executeCommandSpy = jest.spyOn(vscode.commands, 'executeCommand').mockResolvedValue(undefined); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, + update: jest.fn().mockResolvedValue(undefined), inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); + const executeCommandSpy = jest.spyOn(vscode.commands, 'executeCommand').mockResolvedValue(undefined); cpuStates.activate(tracker); // eslint-disable-next-line @typescript-eslint/no-explicit-any (tracker as any)._onWillStartSession.fire(gdbtargetDebugSession); @@ -595,8 +564,6 @@ describe('CpuStates', () => { (cpuStates as any).sessionCpuStates.get(gdbtargetDebugSession.session.id)!.enableCpuStatesFlag = false; await cpuStates.resetViewState(); - expect(updateMock).toHaveBeenCalledWith('vscode-cmsis-debugger.cpuStates.viewState', undefined, vscode.ConfigurationTarget.Workspace); - expect(updateMock).toHaveBeenCalledWith('vscode-cmsis-debugger.cpuStates.viewState', undefined, vscode.ConfigurationTarget.Global); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((cpuStates as any).sessionCpuStates.get(gdbtargetDebugSession.session.id)!.enableCpuStatesFlag).toBe(true); expect(executeCommandSpy).toHaveBeenCalledWith('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index a27332cf..0d6bd282 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -212,7 +212,7 @@ export class CpuStates { if (!states) { return; } - if (!this.activeCpuStates?.enableCpuStatesFlag) { + if (!states.enableCpuStatesFlag) { return; } const newCycles = await session.readMemoryU32(DWT_CYCCNT_ADDRESS); diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index 6019da76..299550be 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -530,17 +530,13 @@ export class ComponentViewerBase { this._componentViewerTreeDataProvider.setRoots(roots); } - private get _settingsKey(): string { - return `${EXTENSION_NAME}.${this._viewId}.viewState`; - } - private async saveCurrentState(): Promise { if (!this._activeSession) { return; } const configStateKey = await this._activeSession.getConfigStateKey(); const filterPattern = this._componentViewerTreeDataProvider.filterPattern; - await writeComponentViewerState( this._settingsKey, configStateKey, this._refreshTimerEnabled, filterPattern); + await writeComponentViewerState( `${EXTENSION_NAME}.${this._viewId}.viewState`, configStateKey, this._refreshTimerEnabled, filterPattern); } private async restorePeriodicUpdateAndFilter(session: GDBTargetDebugSession): Promise { @@ -550,7 +546,7 @@ export class ComponentViewerBase { this._componentViewerTreeDataProvider.setFilter(undefined); vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, false); - const state = readComponentViewerState(this._settingsKey, await session.getConfigStateKey()); + const state = readComponentViewerState(`${EXTENSION_NAME}.${this._viewId}.viewState`, await session.getConfigStateKey()); if (!state) { return; } @@ -568,7 +564,7 @@ export class ComponentViewerBase { public async resetViewState(): Promise { // Clear persisted settings - await clearAllComponentViewerState([this._settingsKey]); + await clearAllComponentViewerState([`${EXTENSION_NAME}.${this._viewId}.viewState`]); // Reset in-memory state to defaults this._refreshTimerEnabled = true; vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index 014f64fd..dce9e6c8 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -1008,96 +1008,10 @@ describe('ComponentViewerBase', () => { }); describe('view state save and restore', () => { - const SETTINGS_KEY = 'vscode-cmsis-debugger.testClass.viewState'; - afterEach(() => { jest.restoreAllMocks(); }); - it('saveCurrentState writes active filter pattern to workspace settings', async () => { - const session = debugSessionFactory('s1'); - (controller as unknown as { _activeSession?: Session })._activeSession = session; - Object.defineProperty(provider, 'filterPattern', { get: () => 'word', configurable: true }); - const updateMock = jest.fn().mockResolvedValue(undefined); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); - await saveCurrentState(); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { s1: { filterPattern: 'word' } }, - vscode.ConfigurationTarget.Workspace - ); - }); - - it('saveCurrentState writes periodicUpdateEnabled=false when auto-refresh is disabled', async () => { - const session = debugSessionFactory('s1'); - (controller as unknown as { _activeSession?: Session })._activeSession = session; - (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; - const updateMock = jest.fn().mockResolvedValue(undefined); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); - await saveCurrentState(); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { s1: { periodicUpdateEnabled: false } }, - vscode.ConfigurationTarget.Workspace - ); - }); - - it('saveCurrentState writes explicit true to workspace when user setting is false (prevents user-level bleed-through)', async () => { - const session = debugSessionFactory('s1'); - (controller as unknown as { _activeSession?: Session })._activeSession = session; - (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = true; - const updateMock = jest.fn().mockResolvedValue(undefined); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ - globalValue: { s1: { periodicUpdateEnabled: false } }, - workspaceValue: {}, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); - await saveCurrentState(); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { s1: { periodicUpdateEnabled: true } }, - vscode.ConfigurationTarget.Workspace - ); - }); - - it('saveCurrentState uses target-type prefix in the configStateKey', async () => { - const session = debugSessionFactory('s1'); - (session.getConfigStateKey as jest.Mock).mockResolvedValue('My-Target::s1'); - (controller as unknown as { _activeSession?: Session })._activeSession = session; - (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; - const updateMock = jest.fn().mockResolvedValue(undefined); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, - inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - const saveCurrentState = (controller as unknown as { saveCurrentState: () => Promise }).saveCurrentState.bind(controller); - await saveCurrentState(); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { 'My-Target::s1': { periodicUpdateEnabled: false } }, - vscode.ConfigurationTarget.Workspace - ); - }); - it('restorePeriodicUpdateAndFilter resets to defaults before applying saved state (prevents session state leaking)', async () => { (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ @@ -1111,7 +1025,6 @@ describe('ComponentViewerBase', () => { }).restorePeriodicUpdateAndFilter.bind(controller); await restoreState(session); - // Default (true) must be applied even though there are no saved settings. expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(true); expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', true); expect(provider.setFilter).toHaveBeenCalledWith(undefined); @@ -1138,47 +1051,19 @@ describe('ComponentViewerBase', () => { expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.filterActive', true); }); - it('restorePeriodicUpdateAndFilter falls back to user settings when workspace has no entry', async () => { + it('resetViewState resets runtime view state', async () => { jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - inspect: jest.fn().mockReturnValue({ - globalValue: { s1: { periodicUpdateEnabled: false } }, - workspaceValue: {}, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - - const session = debugSessionFactory('s1'); - const restoreState = (controller as unknown as { - restorePeriodicUpdateAndFilter: (s: Session) => Promise; - }).restorePeriodicUpdateAndFilter.bind(controller); - await restoreState(session); - - expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(false); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', false); - }); - - it('resetViewState clears persisted settings, resets in-memory state, and unlocks all instances', async () => { - const updateMock = jest.fn().mockResolvedValue(undefined); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - update: updateMock, + update: jest.fn().mockResolvedValue(undefined), inspect: jest.fn().mockReturnValue({ globalValue: {}, workspaceValue: {} }), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); (controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled = false; - const root = makeGuiNode('root'); - const inst = instanceFactory(); - inst.getGuiTree = jest.fn(() => [root]); - (controller as unknown as { _instances: unknown[] })._instances = [ - { componentViewerInstance: inst, lockState: true, sessionId: 's1', dirtyWhileLocked: false }, - ]; await controller.resetViewState(); - expect(updateMock).toHaveBeenCalledWith(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace); - expect(updateMock).toHaveBeenCalledWith(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Global); expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(true); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', true); expect(provider.setFilter).toHaveBeenCalledWith(undefined); - expect((controller as unknown as { _instances: Array<{ lockState: boolean }> })._instances[0].lockState).toBe(false); - expect(root.isLocked).toBe(false); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.filterActive', false); }); }); }); diff --git a/src/views/dynamic-view-states.test.ts b/src/views/dynamic-view-states.test.ts index 7cb9a5b7..0d6f56b6 100644 --- a/src/views/dynamic-view-states.test.ts +++ b/src/views/dynamic-view-states.test.ts @@ -75,7 +75,8 @@ describe('dynamic-view-states', () => { it('writes disabled periodic update state to workspace settings', async () => { const updateMock = mockGetConfiguration(); await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, false, undefined); - expect(updateMock).toHaveBeenCalledWith( SETTINGS_KEY, + expect(updateMock).toHaveBeenCalledWith( + SETTINGS_KEY, { [CONFIG_KEY]: { periodicUpdateEnabled: false, @@ -85,11 +86,9 @@ describe('dynamic-view-states', () => { ); }); - it('removes workspace state when periodic update is enabled and user setting does not conflict', async () => { + it('removes workspace state when periodic update is enabled', async () => { const updateMock = mockGetConfiguration(); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); - expect(updateMock).toHaveBeenCalledWith( SETTINGS_KEY, undefined, @@ -103,9 +102,7 @@ describe('dynamic-view-states', () => { periodicUpdateEnabled: false, }, }); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); - expect(updateMock).toHaveBeenCalledWith( SETTINGS_KEY, { @@ -117,39 +114,6 @@ describe('dynamic-view-states', () => { ); }); - it('writes active filter pattern to workspace settings', async () => { - const updateMock = mockGetConfiguration(); - - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, 'uart'); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { - [CONFIG_KEY]: { - filterPattern: 'uart', - }, - }, - vscode.ConfigurationTarget.Workspace - ); - }); - - it('writes filter pattern together with disabled periodic update state', async () => { - const updateMock = mockGetConfiguration(); - - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, false, 'uart'); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { - [CONFIG_KEY]: { - periodicUpdateEnabled: false, - filterPattern: 'uart', - }, - }, - vscode.ConfigurationTarget.Workspace - ); - }); - it('preserves other workspace entries when writing state', async () => { const otherConfigKey = 'Other-Target::Debug'; const updateMock = mockGetConfiguration( @@ -160,9 +124,7 @@ describe('dynamic-view-states', () => { }, } ); - - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, 'uart'); - + await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, 'user-filter'); expect(updateMock).toHaveBeenCalledWith( SETTINGS_KEY, { @@ -170,7 +132,7 @@ describe('dynamic-view-states', () => { periodicUpdateEnabled: false, }, [CONFIG_KEY]: { - filterPattern: 'uart', + filterPattern: 'user-filter', }, }, vscode.ConfigurationTarget.Workspace @@ -183,9 +145,7 @@ describe('dynamic-view-states', () => { update: updateMock, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - await clearAllComponentViewerState([SETTINGS_KEY]); - expect(updateMock).toHaveBeenCalledWith( SETTINGS_KEY, undefined, @@ -200,11 +160,10 @@ describe('dynamic-view-states', () => { }); describe('CPU States settings', () => { - it('returns user-level value when workspace is empty', () => { + it('returns user-level state when workspace is empty', () => { mockGetConfiguration({ [CONFIG_KEY]: false, }); - expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(false); }); @@ -217,43 +176,14 @@ describe('dynamic-view-states', () => { [CONFIG_KEY]: true, } ); - expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(true); }); - it('writes disabled CPU states value to workspace settings', async () => { - const updateMock = mockGetConfiguration(); - - await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, false); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { - [CONFIG_KEY]: false, - }, - vscode.ConfigurationTarget.Workspace - ); - }); - - it('removes workspace state when CPU states are enabled and user setting does not conflict', async () => { - const updateMock = mockGetConfiguration(); - - await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, true); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - undefined, - vscode.ConfigurationTarget.Workspace - ); - }); - it('writes explicit enabled value when user setting disables CPU states', async () => { const updateMock = mockGetConfiguration({ [CONFIG_KEY]: false, }); - await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, true); - expect(updateMock).toHaveBeenCalledWith( SETTINGS_KEY, { @@ -262,26 +192,5 @@ describe('dynamic-view-states', () => { vscode.ConfigurationTarget.Workspace ); }); - - it('preserves other workspace entries when writing CPU states value', async () => { - const otherConfigKey = 'Other-Target::Debug'; - const updateMock = mockGetConfiguration( - {}, - { - [otherConfigKey]: false, - } - ); - - await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, false); - - expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, - { - [otherConfigKey]: false, - [CONFIG_KEY]: false, - }, - vscode.ConfigurationTarget.Workspace - ); - }); }); }); \ No newline at end of file From f7e9db51f20f156fe60e363421c2837e53518a2f Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Mon, 18 May 2026 14:25:26 +0200 Subject: [PATCH 09/17] Let attached session not take focus --- src/views/component-viewer/component-viewer-base.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index 299550be..8052cf64 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -410,11 +410,11 @@ export class ComponentViewerBase { } private async handleOnConnected(session: GDBTargetDebugSession, tracker: GDBTargetDebugTracker): Promise { - // Update debug session - this._activeSession = session; - vscode.commands.executeCommand('setContext', `${this._viewId}.sessionActive`, true); - // Restore persisted view state (filter + periodic update) - await this.restorePeriodicUpdateAndFilter(session); + if (!this._activeSession) { + // Update debug session during launch connection but not during attach + this._activeSession = session; + vscode.commands.executeCommand('setContext', `${this._viewId}.sessionActive`, true); + } // Load SCVD files from cbuild-run await this.loadScvdFiles(session, tracker); } From ba64849263b3f54da6811eb6fa17f94f1b889011 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Tue, 19 May 2026 13:46:25 +0200 Subject: [PATCH 10/17] Remove redundant code --- package.json | 12 ++++++------ src/views/component-viewer/component-viewer-base.ts | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 495254e3..f954e724 100644 --- a/package.json +++ b/package.json @@ -426,12 +426,12 @@ }, { "command": "vscode-cmsis-debugger.componentViewer.expandAll", - "when": "view == cmsis-debugger.componentViewer && componentViewer.sessionActive", + "when": "view == cmsis-debugger.componentViewer", "group": "navigation@4" }, { "command": "vscode-cmsis-debugger.corePeripherals.expandAll", - "when": "view == cmsis-debugger.corePeripherals && corePeripherals.sessionActive", + "when": "view == cmsis-debugger.corePeripherals", "group": "navigation@4" }, { @@ -456,22 +456,22 @@ }, { "command": "vscode-cmsis-debugger.componentViewer.disablePeriodicUpdate", - "when": "view == cmsis-debugger.componentViewer && componentViewer.periodicUpdateEnabled && componentViewer.sessionActive", + "when": "view == cmsis-debugger.componentViewer && componentViewer.periodicUpdateEnabled", "group": "navigation@3" }, { "command": "vscode-cmsis-debugger.componentViewer.enablePeriodicUpdate", - "when": "view == cmsis-debugger.componentViewer && !componentViewer.periodicUpdateEnabled && componentViewer.sessionActive", + "when": "view == cmsis-debugger.componentViewer && !componentViewer.periodicUpdateEnabled", "group": "navigation@3" }, { "command": "vscode-cmsis-debugger.corePeripherals.disablePeriodicUpdate", - "when": "view == cmsis-debugger.corePeripherals && corePeripherals.periodicUpdateEnabled && corePeripherals.sessionActive", + "when": "view == cmsis-debugger.corePeripherals && corePeripherals.periodicUpdateEnabled", "group": "navigation@3" }, { "command": "vscode-cmsis-debugger.corePeripherals.enablePeriodicUpdate", - "when": "view == cmsis-debugger.corePeripherals && !corePeripherals.periodicUpdateEnabled && corePeripherals.sessionActive", + "when": "view == cmsis-debugger.corePeripherals && !corePeripherals.periodicUpdateEnabled", "group": "navigation@3" } ], diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index 8052cf64..e056c78e 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -413,7 +413,6 @@ export class ComponentViewerBase { if (!this._activeSession) { // Update debug session during launch connection but not during attach this._activeSession = session; - vscode.commands.executeCommand('setContext', `${this._viewId}.sessionActive`, true); } // Load SCVD files from cbuild-run await this.loadScvdFiles(session, tracker); @@ -433,7 +432,6 @@ export class ComponentViewerBase { private async handleOnDidChangeActiveDebugSession(session: GDBTargetDebugSession | undefined): Promise { // Update debug session this._activeSession = session; - vscode.commands.executeCommand('setContext', `${this._viewId}.sessionActive`, !!session); if (session) { await this.restorePeriodicUpdateAndFilter(session); } From 2eb74f74945c70dc8e37136c0a6e76334a9a0652 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 20 May 2026 09:52:12 +0200 Subject: [PATCH 11/17] Unify view state storage --- package.json | 51 +++---- src/features/cpu-states/cpu-states.test.ts | 4 +- src/features/cpu-states/cpu-states.ts | 14 +- .../component-viewer/component-viewer-base.ts | 8 +- .../test/unit/component-viewer-base.test.ts | 23 ++- src/views/dynamic-view-states.test.ts | 144 +++++++++++++----- src/views/dynamic-view-states.ts | 118 +++++++------- 7 files changed, 225 insertions(+), 137 deletions(-) diff --git a/package.json b/package.json index f954e724..b63e3a0f 100644 --- a/package.json +++ b/package.json @@ -581,38 +581,37 @@ } ], "configuration": { - "title": "Arm CMSIS Debugger", + "title": "CMSIS Debugger", "properties": { - "vscode-cmsis-debugger.cpuStates.viewState": { + "vscode-cmsis-debugger.viewState": { "type": "object", - "markdownDescription": "Persisted CPU time enable/disable state per debug configuration.", - "additionalProperties": { - "type": "boolean" - } - }, - "vscode-cmsis-debugger.componentViewer.viewState": { - "type": "object", - "markdownDescription": "Persisted dynamic view state for the Component Viewer.", + "markdownDescription": "Persisted dynamic view state per debug configuration.", "additionalProperties": { "type": "object", "additionalProperties": false, "properties": { - "periodicUpdateEnabled": { "type": "boolean" }, - "filterPattern": { "type": "string" }, - "lockedComponents": { "type": "array", "items": { "type": "string" } } - } - } - }, - "vscode-cmsis-debugger.corePeripherals.viewState": { - "type": "object", - "markdownDescription": "Persisted dynamic view state for the Core Peripherals view.", - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "periodicUpdateEnabled": { "type": "boolean" }, - "filterPattern": { "type": "string" }, - "lockedComponents": { "type": "array", "items": { "type": "string" } } + "componentViewer": { + "type": "object", + "markdownDescription": "Persisted dynamic view state for the Component Viewer.", + "additionalProperties": false, + "properties": { + "periodicUpdateEnabled": { "type": "boolean" }, + "filterPattern": { "type": "string" } + } + }, + "corePeripherals": { + "type": "object", + "markdownDescription": "Persisted dynamic view state for the Core Peripherals view.", + "additionalProperties": false, + "properties": { + "periodicUpdateEnabled": { "type": "boolean" }, + "filterPattern": { "type": "string" } + } + }, + "cpuStates": { + "type": "boolean", + "markdownDescription": "Persisted CPU time enable/disable state." + } } } } diff --git a/src/features/cpu-states/cpu-states.test.ts b/src/features/cpu-states/cpu-states.test.ts index f7e7e81b..5168f816 100644 --- a/src/features/cpu-states/cpu-states.test.ts +++ b/src/features/cpu-states/cpu-states.test.ts @@ -504,7 +504,9 @@ describe('CpuStates', () => { jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ inspect: jest.fn().mockReturnValue({ globalValue: { - 'My-Target::Debug': false, + 'My-Target::Debug': { + cpuStates: false, + }, }, workspaceValue: {}, }), diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 0d6bd282..f9ff3203 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -27,7 +27,7 @@ import { CpuStatesHistory } from './cpu-states-history'; import { calculateTime, extractPname } from '../../utils'; import { GDBTargetConfiguration } from '../../debug-configuration'; import { logger } from '../../logger'; -import {clearAllCpuStatesState, readCpuStatesEnabled, writeCpuStatesEnabled} from '../../views/dynamic-view-states'; +import { clearAllViewState, readCpuStatesEnabled, writeCpuStatesEnabled } from '../../views/dynamic-view-states'; // Architecturally defined registers (M-profile) const DWT_CTRL_ADDRESS = 0xE0001000; @@ -49,8 +49,6 @@ interface SessionCpuStates { } export class CpuStates { - private static readonly SETTINGS_KEY = 'vscode-cmsis-debugger.cpuStates.viewState'; - // onRefresh event to notify GUI components of the cpu states updates (This is different than that of the periodic refresh timer) private readonly _onRefresh: vscode.EventEmitter = new vscode.EventEmitter(); public readonly onRefresh: vscode.Event = this._onRefresh.event; @@ -115,7 +113,7 @@ export class CpuStates { // Refine the key with the target-type prefix and restore the enabled/disabled state from settings.json const configStateKey = await session.getConfigStateKey(); cpuStates.configStateKey = configStateKey; - cpuStates.enableCpuStatesFlag = readCpuStatesEnabled(CpuStates.SETTINGS_KEY, configStateKey) ?? true; + cpuStates.enableCpuStatesFlag = readCpuStatesEnabled(configStateKey) ?? true; // Following call might fail if target not stopped on connect, returns undefined // Retry on first Stopped Event. cpuStates.hasStates = await this.supportsCpuStates(session); @@ -349,7 +347,7 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = true; - await writeCpuStatesEnabled(CpuStates.SETTINGS_KEY, cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); + await writeCpuStatesEnabled(cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } @@ -359,13 +357,13 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = false; - await writeCpuStatesEnabled(CpuStates.SETTINGS_KEY, cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); + await writeCpuStatesEnabled(cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } public async resetViewState(): Promise { - // Clear all persisted cpu states settings from both workspace/user settings.json - await clearAllCpuStatesState([CpuStates.SETTINGS_KEY]); + // Clear persisted settings + await clearAllViewState(); // Re-enable all active sessions in memory for (const states of this.sessionCpuStates.values()) { states.enableCpuStatesFlag = true; diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index e056c78e..c2b3f80e 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -25,7 +25,7 @@ import { perf, parsePerf } from './stats-config'; import { vscodeViewExists } from '../../vscode-utils'; import { EXTENSION_NAME, VIEW_PREFIX } from '../../manifest'; import { ExtendedGDBTargetConfiguration } from '../../debug-configuration/gdbtarget-configuration'; -import { readComponentViewerState, writeComponentViewerState, clearAllComponentViewerState } from '../dynamic-view-states'; +import { clearAllViewState, readComponentViewerState, writeComponentViewerState } from '../dynamic-view-states'; export interface ScvdCollector { getScvdFilePaths(session: GDBTargetDebugSession): Promise; @@ -534,7 +534,7 @@ export class ComponentViewerBase { } const configStateKey = await this._activeSession.getConfigStateKey(); const filterPattern = this._componentViewerTreeDataProvider.filterPattern; - await writeComponentViewerState( `${EXTENSION_NAME}.${this._viewId}.viewState`, configStateKey, this._refreshTimerEnabled, filterPattern); + await writeComponentViewerState(this._viewId, configStateKey, this._refreshTimerEnabled, filterPattern); } private async restorePeriodicUpdateAndFilter(session: GDBTargetDebugSession): Promise { @@ -544,7 +544,7 @@ export class ComponentViewerBase { this._componentViewerTreeDataProvider.setFilter(undefined); vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, false); - const state = readComponentViewerState(`${EXTENSION_NAME}.${this._viewId}.viewState`, await session.getConfigStateKey()); + const state = readComponentViewerState(this._viewId, await session.getConfigStateKey()); if (!state) { return; } @@ -562,7 +562,7 @@ export class ComponentViewerBase { public async resetViewState(): Promise { // Clear persisted settings - await clearAllComponentViewerState([`${EXTENSION_NAME}.${this._viewId}.viewState`]); + await clearAllViewState(); // Reset in-memory state to defaults this._refreshTimerEnabled = true; vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index dce9e6c8..6462635d 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -77,13 +77,14 @@ class TestClass extends ComponentViewerBase { public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider, + viewId = 'testClass', ) { super( context, componentViewerTreeDataProvider, new TestClassScvdCollector(), 'Test Class', - 'testClass'); + viewId); } }; @@ -104,10 +105,12 @@ type ExpansionEventCallback = (event: vscode.TreeViewExpansionEvent new TestClass( context, - provider as ComponentViewerTreeDataProvider + provider as ComponentViewerTreeDataProvider, + viewId ); describe('ComponentViewerBase', () => { @@ -1032,10 +1035,18 @@ describe('ComponentViewerBase', () => { }); it('restorePeriodicUpdateAndFilter restores periodicUpdateEnabled and filter from workspace settings', async () => { + controller = createController(context, provider, 'componentViewer'); jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ inspect: jest.fn().mockReturnValue({ globalValue: {}, - workspaceValue: { s1: { periodicUpdateEnabled: false, filterPattern: 'uart' } }, + workspaceValue: { + s1: { + componentViewer: { + periodicUpdateEnabled: false, + filterPattern: 'uart', + }, + }, + }, }), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); @@ -1046,9 +1057,9 @@ describe('ComponentViewerBase', () => { await restoreState(session); expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(false); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', false); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'componentViewer.periodicUpdateEnabled', false); expect(provider.setFilter).toHaveBeenCalledWith('uart'); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.filterActive', true); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'componentViewer.filterActive', true); }); it('resetViewState resets runtime view state', async () => { diff --git a/src/views/dynamic-view-states.test.ts b/src/views/dynamic-view-states.test.ts index 0d6f56b6..dba18aab 100644 --- a/src/views/dynamic-view-states.test.ts +++ b/src/views/dynamic-view-states.test.ts @@ -16,14 +16,13 @@ import * as vscode from 'vscode'; import { - clearAllComponentViewerState, + clearAllViewState, readComponentViewerState, readCpuStatesEnabled, writeComponentViewerState, writeCpuStatesEnabled, } from './dynamic-view-states'; -const SETTINGS_KEY = 'test.viewState'; const CONFIG_KEY = 'My-Target::Debug'; function mockGetConfiguration(globalValue: Record = {}, workspaceValue: Record = {}): jest.Mock { @@ -45,10 +44,12 @@ describe('dynamic-view-states', () => { it('returns user-level state when workspace is empty', () => { mockGetConfiguration({ [CONFIG_KEY]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, }); - expect(readComponentViewerState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ + expect(readComponentViewerState('componentViewer', CONFIG_KEY)).toEqual({ periodicUpdateEnabled: false, }); }); @@ -57,16 +58,21 @@ describe('dynamic-view-states', () => { mockGetConfiguration( { [CONFIG_KEY]: { - periodicUpdateEnabled: false, - filterPattern: 'user-filter', + componentViewer: { + periodicUpdateEnabled: false, + filterPattern: 'user-filter', + }, }, - }, { + }, + { [CONFIG_KEY]: { - periodicUpdateEnabled: true, + componentViewer: { + periodicUpdateEnabled: true, + }, }, } ); - expect(readComponentViewerState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ + expect(readComponentViewerState('componentViewer', CONFIG_KEY)).toEqual({ periodicUpdateEnabled: true, filterPattern: 'user-filter', }); @@ -74,12 +80,14 @@ describe('dynamic-view-states', () => { it('writes disabled periodic update state to workspace settings', async () => { const updateMock = mockGetConfiguration(); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, false, undefined); + await writeComponentViewerState('componentViewer', CONFIG_KEY, false, undefined); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { [CONFIG_KEY]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, }, vscode.ConfigurationTarget.Workspace @@ -88,9 +96,9 @@ describe('dynamic-view-states', () => { it('removes workspace state when periodic update is enabled', async () => { const updateMock = mockGetConfiguration(); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, undefined); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', undefined, vscode.ConfigurationTarget.Workspace ); @@ -99,60 +107,97 @@ describe('dynamic-view-states', () => { it('writes explicit enabled state when user setting disables periodic update', async () => { const updateMock = mockGetConfiguration({ [CONFIG_KEY]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, }); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, undefined); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { [CONFIG_KEY]: { - periodicUpdateEnabled: true, + componentViewer: { + periodicUpdateEnabled: true, + }, }, }, vscode.ConfigurationTarget.Workspace ); }); + it('does not write explicit enabled state when only workspace disables periodic update', async () => { + const updateMock = mockGetConfiguration( + {}, + { + [CONFIG_KEY]: { + componentViewer: { + periodicUpdateEnabled: false, + }, + }, + } + ); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, undefined); + expect(updateMock).toHaveBeenCalledWith( + 'vscode-cmsis-debugger.viewState', + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); + it('preserves other workspace entries when writing state', async () => { const otherConfigKey = 'Other-Target::Debug'; const updateMock = mockGetConfiguration( {}, { [otherConfigKey]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, } ); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, 'user-filter'); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, 'user-filter'); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { [otherConfigKey]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, [CONFIG_KEY]: { - filterPattern: 'user-filter', + componentViewer: { + filterPattern: 'user-filter', + }, }, }, vscode.ConfigurationTarget.Workspace ); }); + it('ignores unsupported component viewer ids', async () => { + const updateMock = mockGetConfiguration(); + expect(readComponentViewerState('wrongId', CONFIG_KEY)).toBeUndefined(); + + await writeComponentViewerState('wrongId', CONFIG_KEY, false, undefined); + expect(updateMock).not.toHaveBeenCalled(); + }); + it('clears both workspace and global levels', async () => { const updateMock = jest.fn().mockResolvedValue(undefined); jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ update: updateMock, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - await clearAllComponentViewerState([SETTINGS_KEY]); + await clearAllViewState(); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', undefined, vscode.ConfigurationTarget.Workspace ); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', undefined, vscode.ConfigurationTarget.Global ); @@ -162,35 +207,62 @@ describe('dynamic-view-states', () => { describe('CPU States settings', () => { it('returns user-level state when workspace is empty', () => { mockGetConfiguration({ - [CONFIG_KEY]: false, + [CONFIG_KEY]: { + cpuStates: false, + }, }); - expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(false); + expect(readCpuStatesEnabled(CONFIG_KEY)).toBe(false); }); it('uses workspace value before user value when reading', () => { mockGetConfiguration( { - [CONFIG_KEY]: false, + [CONFIG_KEY]: { + cpuStates: false, + }, }, { - [CONFIG_KEY]: true, + [CONFIG_KEY]: { + cpuStates: true, + }, } ); - expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(true); + expect(readCpuStatesEnabled(CONFIG_KEY)).toBe(true); }); it('writes explicit enabled value when user setting disables CPU states', async () => { const updateMock = mockGetConfiguration({ - [CONFIG_KEY]: false, + [CONFIG_KEY]: { + cpuStates: false, + }, }); - await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, true); + await writeCpuStatesEnabled(CONFIG_KEY, true); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { - [CONFIG_KEY]: true, + [CONFIG_KEY]: { + cpuStates: true, + }, }, vscode.ConfigurationTarget.Workspace ); }); + + it('does not write explicit enabled value when only workspace disables CPU states', async () => { + const updateMock = mockGetConfiguration( + {}, + { + [CONFIG_KEY]: { + cpuStates: false, + }, + } + ); + await writeCpuStatesEnabled(CONFIG_KEY, true); + expect(updateMock).toHaveBeenCalledWith( + 'vscode-cmsis-debugger.viewState', + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); }); -}); \ No newline at end of file +}) diff --git a/src/views/dynamic-view-states.ts b/src/views/dynamic-view-states.ts index 302f1ba9..bdd557ed 100644 --- a/src/views/dynamic-view-states.ts +++ b/src/views/dynamic-view-states.ts @@ -16,64 +16,79 @@ import * as vscode from 'vscode'; -type ConfigStateByKey = Record; - -function readConfigState(settingsKey: string, configStateKey: string): T | undefined { - const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); - const globalState = inspection?.globalValue?.[configStateKey]; - const workspaceState = inspection?.workspaceValue?.[configStateKey]; - return workspaceState ?? globalState; -} - -function readMergedConfigState(settingsKey: string, configStateKey: string): T | undefined { - const inspection = vscode.workspace.getConfiguration().inspect>>(settingsKey); - const globalState = inspection?.globalValue?.[configStateKey]; - const workspaceState = inspection?.workspaceValue?.[configStateKey]; - if (globalState === undefined && workspaceState === undefined) { - return undefined; - } +const SETTINGS_KEY = 'vscode-cmsis-debugger.viewState'; + +type ViewStateEntry = { + componentViewer: ComponentViewerState; + corePeripherals: ComponentViewerState; + cpuStates: boolean; +}; +type ViewStateByConfigKey = Record>; + +function readDynamicViewState(configStateKey: string, dynamicView: T, mode: 'global' | 'merged' = 'merged'): ViewStateEntry[T] | undefined { + const inspection = vscode.workspace.getConfiguration().inspect(SETTINGS_KEY); + const globalEntry = inspection?.globalValue?.[configStateKey]; + const workspaceEntry = inspection?.workspaceValue?.[configStateKey]; + const globalViewState = globalEntry?.[dynamicView] as ViewStateEntry[T] | undefined; + const workspaceViewState = workspaceEntry?.[dynamicView] as ViewStateEntry[T] | undefined; + + if (mode === 'global') return globalViewState; // 'User' state provides defaults; 'Workspace' state overrides only the properties it defines. - return { ...(globalState ?? {}), ...(workspaceState ?? {}) } as T; + if (typeof globalViewState === 'object' && globalViewState !== null && typeof workspaceViewState === 'object' && workspaceViewState !== null) { + return { ...globalViewState, ...workspaceViewState } as ViewStateEntry[T]; + } + return workspaceViewState ?? globalViewState; } -async function writeConfigState(settingsKey: string, configStateKey: string, state: T | undefined): Promise { - const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); - const statesToStore = { ...(inspection?.workspaceValue ?? {}) }; +async function writeWorkspaceDynamicViewState(configStateKey: string, dynamicView: T, state: ViewStateEntry[T] | undefined): Promise { + const inspection = vscode.workspace.getConfiguration().inspect(SETTINGS_KEY); + const entriesToStore: ViewStateByConfigKey = { ...(inspection?.workspaceValue ?? {}) }; + const entryToStore: Partial = { ...(entriesToStore[configStateKey] ?? {}) }; if (state === undefined) { - delete statesToStore[configStateKey]; + // Undefined means this dynamic view has no workspace override, so remove it from the stored entry. + delete entryToStore[dynamicView]; + } else { + entryToStore[dynamicView] = state; + } + if (Object.keys(entryToStore).length === 0) { + // Drop empty debug configuration entries instead of leaving empty objects in settings.json. + delete entriesToStore[configStateKey]; } else { - statesToStore[configStateKey] = state; + entriesToStore[configStateKey] = entryToStore; } - const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; - await vscode.workspace.getConfiguration().update(settingsKey, valueToStore, vscode.ConfigurationTarget.Workspace); + // VS Code removes the entire setting when the updated value is undefined. + const valueToStore = Object.keys(entriesToStore).length === 0 ? undefined : entriesToStore; + await vscode.workspace.getConfiguration().update(SETTINGS_KEY, valueToStore, vscode.ConfigurationTarget.Workspace); } -async function clearAllConfigState(settingsKeys: string[]): Promise { - await Promise.all(settingsKeys.flatMap(key => [ - vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Workspace), - vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Global), - ])); +export async function clearAllViewState(): Promise { + await Promise.all([ + vscode.workspace.getConfiguration().update(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace), + vscode.workspace.getConfiguration().update(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Global), + ]); } // ------------------------------------------------------------------------------------------------- -// Component Viewer settings +// Component Viewer and Core Peripherals // ------------------------------------------------------------------------------------------------- -export interface ComponentViewerState { +interface ComponentViewerState { periodicUpdateEnabled?: boolean; filterPattern?: string; } -type ComponentViewerStateByConfig = ConfigStateByKey; - -export function readComponentViewerState(settingsKey: string, configStateKey: string): ComponentViewerState | undefined { - return readMergedConfigState(settingsKey, configStateKey); +export function readComponentViewerState(viewId: string, configStateKey: string): ComponentViewerState | undefined { + if (viewId !== 'componentViewer' && viewId !== 'corePeripherals') { + return undefined; + } + return readDynamicViewState(configStateKey, viewId, 'merged'); } -export async function writeComponentViewerState(settingsKey: string, configStateKey: string, refreshTimerEnabled: boolean, filterPattern: string | undefined -): Promise { - const inspection = vscode.workspace.getConfiguration().inspect(settingsKey); - const userState = inspection?.globalValue?.[configStateKey]; +export async function writeComponentViewerState(viewId: string, configStateKey: string, refreshTimerEnabled: boolean, filterPattern: string | undefined): Promise { + if (viewId !== 'componentViewer' && viewId !== 'corePeripherals') { + return; + } + const userState = readDynamicViewState(configStateKey, viewId, 'global'); // If 'User' settings disable periodicUpdate but this 'Workspace' enables it, // write true explicitly so the 'User' value does not bleed through. const needsExplicitPeriodicUpdate = refreshTimerEnabled && userState?.periodicUpdateEnabled === false; @@ -81,30 +96,21 @@ export async function writeComponentViewerState(settingsKey: string, configState ...(!refreshTimerEnabled || needsExplicitPeriodicUpdate ? { periodicUpdateEnabled: refreshTimerEnabled } : {}), ...(filterPattern !== undefined ? { filterPattern } : {}), }; - await writeConfigState(settingsKey, configStateKey, Object.keys(state).length === 0 ? undefined : state); -} - -export async function clearAllComponentViewerState(settingsKeys: string[]): Promise { - await clearAllConfigState(settingsKeys); + await writeWorkspaceDynamicViewState(configStateKey, viewId, Object.keys(state).length === 0 ? undefined : state); } // ------------------------------------------------------------------------------------------------- -// CPU States settings +// CPU States // ------------------------------------------------------------------------------------------------- -export function readCpuStatesEnabled(settingsKey: string, configStateKey: string): boolean | undefined { - return readConfigState(settingsKey, configStateKey); +export function readCpuStatesEnabled(configStateKey: string): boolean | undefined { + return readDynamicViewState(configStateKey, 'cpuStates', 'merged'); } -export async function writeCpuStatesEnabled(settingsKey: string, configStateKey: string, enabled: boolean): Promise { - const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); - const userState = inspection?.globalValue?.[configStateKey]; - // If 'User' settings disable periodicUpdate but this 'Workspace' enables it, +export async function writeCpuStatesEnabled(configStateKey: string, enabled: boolean): Promise { + const userState = readDynamicViewState(configStateKey, 'cpuStates', 'global'); + // If 'User' settings disable cpuStates but this 'Workspace' enables it, // write true explicitly so the 'User' value does not bleed through. const stateToStore = enabled ? userState === false ? true : undefined : false; - await writeConfigState(settingsKey, configStateKey, stateToStore); -} - -export async function clearAllCpuStatesState(settingsKeys: string[]): Promise { - await clearAllConfigState(settingsKeys); + await writeWorkspaceDynamicViewState(configStateKey, 'cpuStates', stateToStore); } From d90a62f0ae57ace0b6c86a4de6cc0d0260a8c8cf Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 20 May 2026 10:49:59 +0200 Subject: [PATCH 12/17] Refine code --- __mocks__/vscode.js | 1 - src/desktop/extension.ts | 5 ++- src/features/cpu-states/cpu-states.ts | 6 +-- .../component-viewer/component-viewer-base.ts | 6 +-- .../test/unit/component-viewer-base.test.ts | 42 ++++++++----------- 5 files changed, 26 insertions(+), 34 deletions(-) diff --git a/__mocks__/vscode.js b/__mocks__/vscode.js index e908b326..fe469696 100644 --- a/__mocks__/vscode.js +++ b/__mocks__/vscode.js @@ -33,7 +33,6 @@ const StatusBarAlignment = { const ConfigurationTarget = { Global: 1, Workspace: 2, - WorkspaceFolder: 3, }; const MockTreeItemCollapsibleState = { diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index 071cad80..e25ccf7d 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -27,6 +27,7 @@ import { GenericCommands } from '../features/generic-commands'; import { ComponentViewer } from '../views/component-viewer/component-viewer'; import { ComponentViewerTreeDataProvider } from '../views/component-viewer/component-viewer-tree-view'; import { CorePeripherals } from '../views/core-peripherals/core-peripherals'; +import { clearAllViewState } from '../views/dynamic-view-states'; const BUILTIN_TOOLS_PATHS = [ 'tools/pyocd/pyocd', @@ -86,7 +87,9 @@ export const activate = async (context: vscode.ExtensionContext): Promise } // Register reset dynamic view state command - context.subscriptions.push( vscode.commands.registerCommand("vscode-cmsis-debugger.resetDynamicViewState", async () => { + context.subscriptions.push( + vscode.commands.registerCommand('vscode-cmsis-debugger.resetDynamicViewState', async () => { + await clearAllViewState(); await Promise.all([ cpuStates.resetViewState(), componentViewer.resetViewState(), diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index f9ff3203..952588dd 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -27,7 +27,7 @@ import { CpuStatesHistory } from './cpu-states-history'; import { calculateTime, extractPname } from '../../utils'; import { GDBTargetConfiguration } from '../../debug-configuration'; import { logger } from '../../logger'; -import { clearAllViewState, readCpuStatesEnabled, writeCpuStatesEnabled } from '../../views/dynamic-view-states'; +import { readCpuStatesEnabled, writeCpuStatesEnabled } from '../../views/dynamic-view-states'; // Architecturally defined registers (M-profile) const DWT_CTRL_ADDRESS = 0xE0001000; @@ -362,9 +362,7 @@ export class CpuStates { } public async resetViewState(): Promise { - // Clear persisted settings - await clearAllViewState(); - // Re-enable all active sessions in memory + // Re-enable all active sessions in memory. for (const states of this.sessionCpuStates.values()) { states.enableCpuStatesFlag = true; } diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index c2b3f80e..05dd43a0 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -25,7 +25,7 @@ import { perf, parsePerf } from './stats-config'; import { vscodeViewExists } from '../../vscode-utils'; import { EXTENSION_NAME, VIEW_PREFIX } from '../../manifest'; import { ExtendedGDBTargetConfiguration } from '../../debug-configuration/gdbtarget-configuration'; -import { clearAllViewState, readComponentViewerState, writeComponentViewerState } from '../dynamic-view-states'; +import { readComponentViewerState, writeComponentViewerState } from '../dynamic-view-states'; export interface ScvdCollector { getScvdFilePaths(session: GDBTargetDebugSession): Promise; @@ -561,9 +561,7 @@ export class ComponentViewerBase { } public async resetViewState(): Promise { - // Clear persisted settings - await clearAllViewState(); - // Reset in-memory state to defaults + // Reset in-memory state to defaults. this._refreshTimerEnabled = true; vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); this._componentViewerTreeDataProvider.setFilter(undefined); diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index 6462635d..b476699a 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -30,6 +30,7 @@ import { ComponentViewerBase } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; import { debugSessionFactory, trackerFactory, OnRefreshCallback, Session, TrackerCallbacks } from '../../../../debug-session/__test__/debug-session.factory'; +import { readComponentViewerState, writeComponentViewerState } from '../../../dynamic-view-states'; const instanceFactory = jest.fn(() => ({ @@ -59,6 +60,11 @@ jest.mock('../../../../logger', () => ({ }, })); +jest.mock('../../../dynamic-view-states', () => ({ + readComponentViewerState: jest.fn(), + writeComponentViewerState: jest.fn().mockResolvedValue(undefined), +})); + function asMockedFunction( fn: (...args: Args) => Return ): jest.MockedFunction<(...args: Args) => Return> { @@ -77,14 +83,13 @@ class TestClass extends ComponentViewerBase { public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider, - viewId = 'testClass', ) { super( context, componentViewerTreeDataProvider, new TestClassScvdCollector(), 'Test Class', - viewId); + 'testClass'); } }; @@ -105,12 +110,10 @@ type ExpansionEventCallback = (event: vscode.TreeViewExpansionEvent new TestClass( context, - provider as ComponentViewerTreeDataProvider, - viewId + provider as ComponentViewerTreeDataProvider ); describe('ComponentViewerBase', () => { @@ -125,6 +128,7 @@ describe('ComponentViewerBase', () => { provider = treeDataProviderFactory(); tracker = trackerFactory(); controller = createController(context, provider); + asMockedFunction(readComponentViewerState).mockReturnValue(undefined); // Extend registered commands for test class. const defaultMockedCommands = await vscode.commands.getCommands(); asMockedFunction(vscode.commands.getCommands).mockResolvedValue([ @@ -1035,31 +1039,21 @@ describe('ComponentViewerBase', () => { }); it('restorePeriodicUpdateAndFilter restores periodicUpdateEnabled and filter from workspace settings', async () => { - controller = createController(context, provider, 'componentViewer'); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - inspect: jest.fn().mockReturnValue({ - globalValue: {}, - workspaceValue: { - s1: { - componentViewer: { - periodicUpdateEnabled: false, - filterPattern: 'uart', - }, - }, - }, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); + asMockedFunction(readComponentViewerState).mockReturnValue({ + periodicUpdateEnabled: false, + filterPattern: 'word', + }); const session = debugSessionFactory('s1'); const restoreState = (controller as unknown as { restorePeriodicUpdateAndFilter: (s: Session) => Promise; }).restorePeriodicUpdateAndFilter.bind(controller); await restoreState(session); + expect(readComponentViewerState).toHaveBeenCalledWith('testClass', 's1'); expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(false); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'componentViewer.periodicUpdateEnabled', false); - expect(provider.setFilter).toHaveBeenCalledWith('uart'); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'componentViewer.filterActive', true); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', false); + expect(provider.setFilter).toHaveBeenCalledWith('word'); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.filterActive', true); }); it('resetViewState resets runtime view state', async () => { From 6852beb654d1e4e53e4d9bbe830dd381ad5e9187 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 20 May 2026 09:52:12 +0200 Subject: [PATCH 13/17] Unify view state storage --- __mocks__/vscode.js | 1 - package.json | 51 +++---- src/desktop/extension.ts | 5 +- src/features/cpu-states/cpu-states.test.ts | 4 +- src/features/cpu-states/cpu-states.ts | 14 +- .../component-viewer/component-viewer-base.ts | 18 +-- .../test/unit/component-viewer-base.test.ts | 21 ++- src/views/dynamic-view-states.test.ts | 144 +++++++++++++----- src/views/dynamic-view-states.ts | 118 +++++++------- 9 files changed, 228 insertions(+), 148 deletions(-) diff --git a/__mocks__/vscode.js b/__mocks__/vscode.js index e908b326..fe469696 100644 --- a/__mocks__/vscode.js +++ b/__mocks__/vscode.js @@ -33,7 +33,6 @@ const StatusBarAlignment = { const ConfigurationTarget = { Global: 1, Workspace: 2, - WorkspaceFolder: 3, }; const MockTreeItemCollapsibleState = { diff --git a/package.json b/package.json index f954e724..b63e3a0f 100644 --- a/package.json +++ b/package.json @@ -581,38 +581,37 @@ } ], "configuration": { - "title": "Arm CMSIS Debugger", + "title": "CMSIS Debugger", "properties": { - "vscode-cmsis-debugger.cpuStates.viewState": { + "vscode-cmsis-debugger.viewState": { "type": "object", - "markdownDescription": "Persisted CPU time enable/disable state per debug configuration.", - "additionalProperties": { - "type": "boolean" - } - }, - "vscode-cmsis-debugger.componentViewer.viewState": { - "type": "object", - "markdownDescription": "Persisted dynamic view state for the Component Viewer.", + "markdownDescription": "Persisted dynamic view state per debug configuration.", "additionalProperties": { "type": "object", "additionalProperties": false, "properties": { - "periodicUpdateEnabled": { "type": "boolean" }, - "filterPattern": { "type": "string" }, - "lockedComponents": { "type": "array", "items": { "type": "string" } } - } - } - }, - "vscode-cmsis-debugger.corePeripherals.viewState": { - "type": "object", - "markdownDescription": "Persisted dynamic view state for the Core Peripherals view.", - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "periodicUpdateEnabled": { "type": "boolean" }, - "filterPattern": { "type": "string" }, - "lockedComponents": { "type": "array", "items": { "type": "string" } } + "componentViewer": { + "type": "object", + "markdownDescription": "Persisted dynamic view state for the Component Viewer.", + "additionalProperties": false, + "properties": { + "periodicUpdateEnabled": { "type": "boolean" }, + "filterPattern": { "type": "string" } + } + }, + "corePeripherals": { + "type": "object", + "markdownDescription": "Persisted dynamic view state for the Core Peripherals view.", + "additionalProperties": false, + "properties": { + "periodicUpdateEnabled": { "type": "boolean" }, + "filterPattern": { "type": "string" } + } + }, + "cpuStates": { + "type": "boolean", + "markdownDescription": "Persisted CPU time enable/disable state." + } } } } diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index 071cad80..e25ccf7d 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -27,6 +27,7 @@ import { GenericCommands } from '../features/generic-commands'; import { ComponentViewer } from '../views/component-viewer/component-viewer'; import { ComponentViewerTreeDataProvider } from '../views/component-viewer/component-viewer-tree-view'; import { CorePeripherals } from '../views/core-peripherals/core-peripherals'; +import { clearAllViewState } from '../views/dynamic-view-states'; const BUILTIN_TOOLS_PATHS = [ 'tools/pyocd/pyocd', @@ -86,7 +87,9 @@ export const activate = async (context: vscode.ExtensionContext): Promise } // Register reset dynamic view state command - context.subscriptions.push( vscode.commands.registerCommand("vscode-cmsis-debugger.resetDynamicViewState", async () => { + context.subscriptions.push( + vscode.commands.registerCommand('vscode-cmsis-debugger.resetDynamicViewState', async () => { + await clearAllViewState(); await Promise.all([ cpuStates.resetViewState(), componentViewer.resetViewState(), diff --git a/src/features/cpu-states/cpu-states.test.ts b/src/features/cpu-states/cpu-states.test.ts index f7e7e81b..5168f816 100644 --- a/src/features/cpu-states/cpu-states.test.ts +++ b/src/features/cpu-states/cpu-states.test.ts @@ -504,7 +504,9 @@ describe('CpuStates', () => { jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ inspect: jest.fn().mockReturnValue({ globalValue: { - 'My-Target::Debug': false, + 'My-Target::Debug': { + cpuStates: false, + }, }, workspaceValue: {}, }), diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 0d6bd282..952588dd 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -27,7 +27,7 @@ import { CpuStatesHistory } from './cpu-states-history'; import { calculateTime, extractPname } from '../../utils'; import { GDBTargetConfiguration } from '../../debug-configuration'; import { logger } from '../../logger'; -import {clearAllCpuStatesState, readCpuStatesEnabled, writeCpuStatesEnabled} from '../../views/dynamic-view-states'; +import { readCpuStatesEnabled, writeCpuStatesEnabled } from '../../views/dynamic-view-states'; // Architecturally defined registers (M-profile) const DWT_CTRL_ADDRESS = 0xE0001000; @@ -49,8 +49,6 @@ interface SessionCpuStates { } export class CpuStates { - private static readonly SETTINGS_KEY = 'vscode-cmsis-debugger.cpuStates.viewState'; - // onRefresh event to notify GUI components of the cpu states updates (This is different than that of the periodic refresh timer) private readonly _onRefresh: vscode.EventEmitter = new vscode.EventEmitter(); public readonly onRefresh: vscode.Event = this._onRefresh.event; @@ -115,7 +113,7 @@ export class CpuStates { // Refine the key with the target-type prefix and restore the enabled/disabled state from settings.json const configStateKey = await session.getConfigStateKey(); cpuStates.configStateKey = configStateKey; - cpuStates.enableCpuStatesFlag = readCpuStatesEnabled(CpuStates.SETTINGS_KEY, configStateKey) ?? true; + cpuStates.enableCpuStatesFlag = readCpuStatesEnabled(configStateKey) ?? true; // Following call might fail if target not stopped on connect, returns undefined // Retry on first Stopped Event. cpuStates.hasStates = await this.supportsCpuStates(session); @@ -349,7 +347,7 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = true; - await writeCpuStatesEnabled(CpuStates.SETTINGS_KEY, cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); + await writeCpuStatesEnabled(cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } @@ -359,14 +357,12 @@ export class CpuStates { return; } cpuStates.enableCpuStatesFlag = false; - await writeCpuStatesEnabled(CpuStates.SETTINGS_KEY, cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); + await writeCpuStatesEnabled(cpuStates.configStateKey, cpuStates.enableCpuStatesFlag); await vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); } public async resetViewState(): Promise { - // Clear all persisted cpu states settings from both workspace/user settings.json - await clearAllCpuStatesState([CpuStates.SETTINGS_KEY]); - // Re-enable all active sessions in memory + // Re-enable all active sessions in memory. for (const states of this.sessionCpuStates.values()) { states.enableCpuStatesFlag = true; } diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index e056c78e..e95b9ceb 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -25,7 +25,7 @@ import { perf, parsePerf } from './stats-config'; import { vscodeViewExists } from '../../vscode-utils'; import { EXTENSION_NAME, VIEW_PREFIX } from '../../manifest'; import { ExtendedGDBTargetConfiguration } from '../../debug-configuration/gdbtarget-configuration'; -import { readComponentViewerState, writeComponentViewerState, clearAllComponentViewerState } from '../dynamic-view-states'; +import { readComponentViewerState, writeComponentViewerState } from '../dynamic-view-states'; export interface ScvdCollector { getScvdFilePaths(session: GDBTargetDebugSession): Promise; @@ -104,14 +104,14 @@ export class ComponentViewerBase { const enablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.enablePeriodicUpdate`, async () => { this._refreshTimerEnabled = true; componentViewerLogger.info(`${this._viewName}: Auto refresh enabled`); - vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); - this.saveCurrentState(); + await vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); + await this.saveCurrentState(); }); const disablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.disablePeriodicUpdate`, async () => { this._refreshTimerEnabled = false; componentViewerLogger.info(`${this._viewName}: Auto refresh disabled`); - vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, false); - this.saveCurrentState(); + await vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, false); + await this.saveCurrentState(); }); const expandAllCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.expandAll`, async () => { componentViewerLogger.debug(`${this._viewName}: Expand all tree items`); @@ -534,7 +534,7 @@ export class ComponentViewerBase { } const configStateKey = await this._activeSession.getConfigStateKey(); const filterPattern = this._componentViewerTreeDataProvider.filterPattern; - await writeComponentViewerState( `${EXTENSION_NAME}.${this._viewId}.viewState`, configStateKey, this._refreshTimerEnabled, filterPattern); + await writeComponentViewerState(this._viewId, configStateKey, this._refreshTimerEnabled, filterPattern); } private async restorePeriodicUpdateAndFilter(session: GDBTargetDebugSession): Promise { @@ -544,7 +544,7 @@ export class ComponentViewerBase { this._componentViewerTreeDataProvider.setFilter(undefined); vscode.commands.executeCommand('setContext', `${this._viewId}.filterActive`, false); - const state = readComponentViewerState(`${EXTENSION_NAME}.${this._viewId}.viewState`, await session.getConfigStateKey()); + const state = readComponentViewerState(this._viewId, await session.getConfigStateKey()); if (!state) { return; } @@ -561,9 +561,7 @@ export class ComponentViewerBase { } public async resetViewState(): Promise { - // Clear persisted settings - await clearAllComponentViewerState([`${EXTENSION_NAME}.${this._viewId}.viewState`]); - // Reset in-memory state to defaults + // Reset in-memory state to defaults. this._refreshTimerEnabled = true; vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); this._componentViewerTreeDataProvider.setFilter(undefined); diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index dce9e6c8..e73d499e 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -30,6 +30,7 @@ import { ComponentViewerBase } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; import { debugSessionFactory, trackerFactory, OnRefreshCallback, Session, TrackerCallbacks } from '../../../../debug-session/__test__/debug-session.factory'; +import { readComponentViewerState } from '../../../dynamic-view-states'; const instanceFactory = jest.fn(() => ({ @@ -59,6 +60,11 @@ jest.mock('../../../../logger', () => ({ }, })); +jest.mock('../../../dynamic-view-states', () => ({ + readComponentViewerState: jest.fn(), + writeComponentViewerState: jest.fn().mockResolvedValue(undefined), +})); + function asMockedFunction( fn: (...args: Args) => Return ): jest.MockedFunction<(...args: Args) => Return> { @@ -122,6 +128,7 @@ describe('ComponentViewerBase', () => { provider = treeDataProviderFactory(); tracker = trackerFactory(); controller = createController(context, provider); + asMockedFunction(readComponentViewerState).mockReturnValue(undefined); // Extend registered commands for test class. const defaultMockedCommands = await vscode.commands.getCommands(); asMockedFunction(vscode.commands.getCommands).mockResolvedValue([ @@ -1032,22 +1039,20 @@ describe('ComponentViewerBase', () => { }); it('restorePeriodicUpdateAndFilter restores periodicUpdateEnabled and filter from workspace settings', async () => { - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - inspect: jest.fn().mockReturnValue({ - globalValue: {}, - workspaceValue: { s1: { periodicUpdateEnabled: false, filterPattern: 'uart' } }, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); + asMockedFunction(readComponentViewerState).mockReturnValue({ + periodicUpdateEnabled: false, + filterPattern: 'word', + }); const session = debugSessionFactory('s1'); const restoreState = (controller as unknown as { restorePeriodicUpdateAndFilter: (s: Session) => Promise; }).restorePeriodicUpdateAndFilter.bind(controller); await restoreState(session); + expect(readComponentViewerState).toHaveBeenCalledWith('testClass', 's1'); expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(false); expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.periodicUpdateEnabled', false); - expect(provider.setFilter).toHaveBeenCalledWith('uart'); + expect(provider.setFilter).toHaveBeenCalledWith('word'); expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'testClass.filterActive', true); }); diff --git a/src/views/dynamic-view-states.test.ts b/src/views/dynamic-view-states.test.ts index 0d6f56b6..dba18aab 100644 --- a/src/views/dynamic-view-states.test.ts +++ b/src/views/dynamic-view-states.test.ts @@ -16,14 +16,13 @@ import * as vscode from 'vscode'; import { - clearAllComponentViewerState, + clearAllViewState, readComponentViewerState, readCpuStatesEnabled, writeComponentViewerState, writeCpuStatesEnabled, } from './dynamic-view-states'; -const SETTINGS_KEY = 'test.viewState'; const CONFIG_KEY = 'My-Target::Debug'; function mockGetConfiguration(globalValue: Record = {}, workspaceValue: Record = {}): jest.Mock { @@ -45,10 +44,12 @@ describe('dynamic-view-states', () => { it('returns user-level state when workspace is empty', () => { mockGetConfiguration({ [CONFIG_KEY]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, }); - expect(readComponentViewerState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ + expect(readComponentViewerState('componentViewer', CONFIG_KEY)).toEqual({ periodicUpdateEnabled: false, }); }); @@ -57,16 +58,21 @@ describe('dynamic-view-states', () => { mockGetConfiguration( { [CONFIG_KEY]: { - periodicUpdateEnabled: false, - filterPattern: 'user-filter', + componentViewer: { + periodicUpdateEnabled: false, + filterPattern: 'user-filter', + }, }, - }, { + }, + { [CONFIG_KEY]: { - periodicUpdateEnabled: true, + componentViewer: { + periodicUpdateEnabled: true, + }, }, } ); - expect(readComponentViewerState(SETTINGS_KEY, CONFIG_KEY)).toEqual({ + expect(readComponentViewerState('componentViewer', CONFIG_KEY)).toEqual({ periodicUpdateEnabled: true, filterPattern: 'user-filter', }); @@ -74,12 +80,14 @@ describe('dynamic-view-states', () => { it('writes disabled periodic update state to workspace settings', async () => { const updateMock = mockGetConfiguration(); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, false, undefined); + await writeComponentViewerState('componentViewer', CONFIG_KEY, false, undefined); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { [CONFIG_KEY]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, }, vscode.ConfigurationTarget.Workspace @@ -88,9 +96,9 @@ describe('dynamic-view-states', () => { it('removes workspace state when periodic update is enabled', async () => { const updateMock = mockGetConfiguration(); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, undefined); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', undefined, vscode.ConfigurationTarget.Workspace ); @@ -99,60 +107,97 @@ describe('dynamic-view-states', () => { it('writes explicit enabled state when user setting disables periodic update', async () => { const updateMock = mockGetConfiguration({ [CONFIG_KEY]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, }); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, undefined); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, undefined); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { [CONFIG_KEY]: { - periodicUpdateEnabled: true, + componentViewer: { + periodicUpdateEnabled: true, + }, }, }, vscode.ConfigurationTarget.Workspace ); }); + it('does not write explicit enabled state when only workspace disables periodic update', async () => { + const updateMock = mockGetConfiguration( + {}, + { + [CONFIG_KEY]: { + componentViewer: { + periodicUpdateEnabled: false, + }, + }, + } + ); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, undefined); + expect(updateMock).toHaveBeenCalledWith( + 'vscode-cmsis-debugger.viewState', + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); + it('preserves other workspace entries when writing state', async () => { const otherConfigKey = 'Other-Target::Debug'; const updateMock = mockGetConfiguration( {}, { [otherConfigKey]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, } ); - await writeComponentViewerState(SETTINGS_KEY, CONFIG_KEY, true, 'user-filter'); + await writeComponentViewerState('componentViewer', CONFIG_KEY, true, 'user-filter'); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { [otherConfigKey]: { - periodicUpdateEnabled: false, + componentViewer: { + periodicUpdateEnabled: false, + }, }, [CONFIG_KEY]: { - filterPattern: 'user-filter', + componentViewer: { + filterPattern: 'user-filter', + }, }, }, vscode.ConfigurationTarget.Workspace ); }); + it('ignores unsupported component viewer ids', async () => { + const updateMock = mockGetConfiguration(); + expect(readComponentViewerState('wrongId', CONFIG_KEY)).toBeUndefined(); + + await writeComponentViewerState('wrongId', CONFIG_KEY, false, undefined); + expect(updateMock).not.toHaveBeenCalled(); + }); + it('clears both workspace and global levels', async () => { const updateMock = jest.fn().mockResolvedValue(undefined); jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ update: updateMock, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - await clearAllComponentViewerState([SETTINGS_KEY]); + await clearAllViewState(); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', undefined, vscode.ConfigurationTarget.Workspace ); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', undefined, vscode.ConfigurationTarget.Global ); @@ -162,35 +207,62 @@ describe('dynamic-view-states', () => { describe('CPU States settings', () => { it('returns user-level state when workspace is empty', () => { mockGetConfiguration({ - [CONFIG_KEY]: false, + [CONFIG_KEY]: { + cpuStates: false, + }, }); - expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(false); + expect(readCpuStatesEnabled(CONFIG_KEY)).toBe(false); }); it('uses workspace value before user value when reading', () => { mockGetConfiguration( { - [CONFIG_KEY]: false, + [CONFIG_KEY]: { + cpuStates: false, + }, }, { - [CONFIG_KEY]: true, + [CONFIG_KEY]: { + cpuStates: true, + }, } ); - expect(readCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY)).toBe(true); + expect(readCpuStatesEnabled(CONFIG_KEY)).toBe(true); }); it('writes explicit enabled value when user setting disables CPU states', async () => { const updateMock = mockGetConfiguration({ - [CONFIG_KEY]: false, + [CONFIG_KEY]: { + cpuStates: false, + }, }); - await writeCpuStatesEnabled(SETTINGS_KEY, CONFIG_KEY, true); + await writeCpuStatesEnabled(CONFIG_KEY, true); expect(updateMock).toHaveBeenCalledWith( - SETTINGS_KEY, + 'vscode-cmsis-debugger.viewState', { - [CONFIG_KEY]: true, + [CONFIG_KEY]: { + cpuStates: true, + }, }, vscode.ConfigurationTarget.Workspace ); }); + + it('does not write explicit enabled value when only workspace disables CPU states', async () => { + const updateMock = mockGetConfiguration( + {}, + { + [CONFIG_KEY]: { + cpuStates: false, + }, + } + ); + await writeCpuStatesEnabled(CONFIG_KEY, true); + expect(updateMock).toHaveBeenCalledWith( + 'vscode-cmsis-debugger.viewState', + undefined, + vscode.ConfigurationTarget.Workspace + ); + }); }); -}); \ No newline at end of file +}) diff --git a/src/views/dynamic-view-states.ts b/src/views/dynamic-view-states.ts index 302f1ba9..bdd557ed 100644 --- a/src/views/dynamic-view-states.ts +++ b/src/views/dynamic-view-states.ts @@ -16,64 +16,79 @@ import * as vscode from 'vscode'; -type ConfigStateByKey = Record; - -function readConfigState(settingsKey: string, configStateKey: string): T | undefined { - const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); - const globalState = inspection?.globalValue?.[configStateKey]; - const workspaceState = inspection?.workspaceValue?.[configStateKey]; - return workspaceState ?? globalState; -} - -function readMergedConfigState(settingsKey: string, configStateKey: string): T | undefined { - const inspection = vscode.workspace.getConfiguration().inspect>>(settingsKey); - const globalState = inspection?.globalValue?.[configStateKey]; - const workspaceState = inspection?.workspaceValue?.[configStateKey]; - if (globalState === undefined && workspaceState === undefined) { - return undefined; - } +const SETTINGS_KEY = 'vscode-cmsis-debugger.viewState'; + +type ViewStateEntry = { + componentViewer: ComponentViewerState; + corePeripherals: ComponentViewerState; + cpuStates: boolean; +}; +type ViewStateByConfigKey = Record>; + +function readDynamicViewState(configStateKey: string, dynamicView: T, mode: 'global' | 'merged' = 'merged'): ViewStateEntry[T] | undefined { + const inspection = vscode.workspace.getConfiguration().inspect(SETTINGS_KEY); + const globalEntry = inspection?.globalValue?.[configStateKey]; + const workspaceEntry = inspection?.workspaceValue?.[configStateKey]; + const globalViewState = globalEntry?.[dynamicView] as ViewStateEntry[T] | undefined; + const workspaceViewState = workspaceEntry?.[dynamicView] as ViewStateEntry[T] | undefined; + + if (mode === 'global') return globalViewState; // 'User' state provides defaults; 'Workspace' state overrides only the properties it defines. - return { ...(globalState ?? {}), ...(workspaceState ?? {}) } as T; + if (typeof globalViewState === 'object' && globalViewState !== null && typeof workspaceViewState === 'object' && workspaceViewState !== null) { + return { ...globalViewState, ...workspaceViewState } as ViewStateEntry[T]; + } + return workspaceViewState ?? globalViewState; } -async function writeConfigState(settingsKey: string, configStateKey: string, state: T | undefined): Promise { - const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); - const statesToStore = { ...(inspection?.workspaceValue ?? {}) }; +async function writeWorkspaceDynamicViewState(configStateKey: string, dynamicView: T, state: ViewStateEntry[T] | undefined): Promise { + const inspection = vscode.workspace.getConfiguration().inspect(SETTINGS_KEY); + const entriesToStore: ViewStateByConfigKey = { ...(inspection?.workspaceValue ?? {}) }; + const entryToStore: Partial = { ...(entriesToStore[configStateKey] ?? {}) }; if (state === undefined) { - delete statesToStore[configStateKey]; + // Undefined means this dynamic view has no workspace override, so remove it from the stored entry. + delete entryToStore[dynamicView]; + } else { + entryToStore[dynamicView] = state; + } + if (Object.keys(entryToStore).length === 0) { + // Drop empty debug configuration entries instead of leaving empty objects in settings.json. + delete entriesToStore[configStateKey]; } else { - statesToStore[configStateKey] = state; + entriesToStore[configStateKey] = entryToStore; } - const valueToStore = Object.keys(statesToStore).length === 0 ? undefined : statesToStore; - await vscode.workspace.getConfiguration().update(settingsKey, valueToStore, vscode.ConfigurationTarget.Workspace); + // VS Code removes the entire setting when the updated value is undefined. + const valueToStore = Object.keys(entriesToStore).length === 0 ? undefined : entriesToStore; + await vscode.workspace.getConfiguration().update(SETTINGS_KEY, valueToStore, vscode.ConfigurationTarget.Workspace); } -async function clearAllConfigState(settingsKeys: string[]): Promise { - await Promise.all(settingsKeys.flatMap(key => [ - vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Workspace), - vscode.workspace.getConfiguration().update(key, undefined, vscode.ConfigurationTarget.Global), - ])); +export async function clearAllViewState(): Promise { + await Promise.all([ + vscode.workspace.getConfiguration().update(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Workspace), + vscode.workspace.getConfiguration().update(SETTINGS_KEY, undefined, vscode.ConfigurationTarget.Global), + ]); } // ------------------------------------------------------------------------------------------------- -// Component Viewer settings +// Component Viewer and Core Peripherals // ------------------------------------------------------------------------------------------------- -export interface ComponentViewerState { +interface ComponentViewerState { periodicUpdateEnabled?: boolean; filterPattern?: string; } -type ComponentViewerStateByConfig = ConfigStateByKey; - -export function readComponentViewerState(settingsKey: string, configStateKey: string): ComponentViewerState | undefined { - return readMergedConfigState(settingsKey, configStateKey); +export function readComponentViewerState(viewId: string, configStateKey: string): ComponentViewerState | undefined { + if (viewId !== 'componentViewer' && viewId !== 'corePeripherals') { + return undefined; + } + return readDynamicViewState(configStateKey, viewId, 'merged'); } -export async function writeComponentViewerState(settingsKey: string, configStateKey: string, refreshTimerEnabled: boolean, filterPattern: string | undefined -): Promise { - const inspection = vscode.workspace.getConfiguration().inspect(settingsKey); - const userState = inspection?.globalValue?.[configStateKey]; +export async function writeComponentViewerState(viewId: string, configStateKey: string, refreshTimerEnabled: boolean, filterPattern: string | undefined): Promise { + if (viewId !== 'componentViewer' && viewId !== 'corePeripherals') { + return; + } + const userState = readDynamicViewState(configStateKey, viewId, 'global'); // If 'User' settings disable periodicUpdate but this 'Workspace' enables it, // write true explicitly so the 'User' value does not bleed through. const needsExplicitPeriodicUpdate = refreshTimerEnabled && userState?.periodicUpdateEnabled === false; @@ -81,30 +96,21 @@ export async function writeComponentViewerState(settingsKey: string, configState ...(!refreshTimerEnabled || needsExplicitPeriodicUpdate ? { periodicUpdateEnabled: refreshTimerEnabled } : {}), ...(filterPattern !== undefined ? { filterPattern } : {}), }; - await writeConfigState(settingsKey, configStateKey, Object.keys(state).length === 0 ? undefined : state); -} - -export async function clearAllComponentViewerState(settingsKeys: string[]): Promise { - await clearAllConfigState(settingsKeys); + await writeWorkspaceDynamicViewState(configStateKey, viewId, Object.keys(state).length === 0 ? undefined : state); } // ------------------------------------------------------------------------------------------------- -// CPU States settings +// CPU States // ------------------------------------------------------------------------------------------------- -export function readCpuStatesEnabled(settingsKey: string, configStateKey: string): boolean | undefined { - return readConfigState(settingsKey, configStateKey); +export function readCpuStatesEnabled(configStateKey: string): boolean | undefined { + return readDynamicViewState(configStateKey, 'cpuStates', 'merged'); } -export async function writeCpuStatesEnabled(settingsKey: string, configStateKey: string, enabled: boolean): Promise { - const inspection = vscode.workspace.getConfiguration().inspect>(settingsKey); - const userState = inspection?.globalValue?.[configStateKey]; - // If 'User' settings disable periodicUpdate but this 'Workspace' enables it, +export async function writeCpuStatesEnabled(configStateKey: string, enabled: boolean): Promise { + const userState = readDynamicViewState(configStateKey, 'cpuStates', 'global'); + // If 'User' settings disable cpuStates but this 'Workspace' enables it, // write true explicitly so the 'User' value does not bleed through. const stateToStore = enabled ? userState === false ? true : undefined : false; - await writeConfigState(settingsKey, configStateKey, stateToStore); -} - -export async function clearAllCpuStatesState(settingsKeys: string[]): Promise { - await clearAllConfigState(settingsKeys); + await writeWorkspaceDynamicViewState(configStateKey, 'cpuStates', stateToStore); } From 898267ca88b62729bd36c42d84f9a286a7fd9a84 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 20 May 2026 13:23:06 +0200 Subject: [PATCH 14/17] Add suggestions by Copilot --- src/features/cpu-states/cpu-states.ts | 5 ++++- .../component-viewer/component-viewer-base.ts | 15 ++++++++------- .../test/unit/component-viewer-base.test.ts | 5 ----- src/views/dynamic-view-states.test.ts | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 952588dd..211e3bc0 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -114,6 +114,9 @@ export class CpuStates { const configStateKey = await session.getConfigStateKey(); cpuStates.configStateKey = configStateKey; cpuStates.enableCpuStatesFlag = readCpuStatesEnabled(configStateKey) ?? true; + if (this.activeSession?.session.id === session.session.id) { + void vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', cpuStates.enableCpuStatesFlag); + } // Following call might fail if target not stopped on connect, returns undefined // Retry on first Stopped Event. cpuStates.hasStates = await this.supportsCpuStates(session); @@ -367,6 +370,6 @@ export class CpuStates { states.enableCpuStatesFlag = true; } vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); - logger.info("CPU States: CPU Timer reset"); + logger.info('CPU States: CPU Timer reset'); } }; diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index e95b9ceb..685c098e 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -103,15 +103,16 @@ export class ComponentViewerBase { }); const enablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.enablePeriodicUpdate`, async () => { this._refreshTimerEnabled = true; - componentViewerLogger.info(`${this._viewName}: Auto refresh enabled`); - await vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); await this.saveCurrentState(); + await vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); + componentViewerLogger.info(`${this._viewName}: Auto refresh enabled`); }); const disablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.disablePeriodicUpdate`, async () => { this._refreshTimerEnabled = false; - componentViewerLogger.info(`${this._viewName}: Auto refresh disabled`); - await vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, false); await this.saveCurrentState(); + await vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, false); + componentViewerLogger.info(`${this._viewName}: Auto refresh disabled`); + }); const expandAllCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.expandAll`, async () => { componentViewerLogger.debug(`${this._viewName}: Expand all tree items`); @@ -135,7 +136,7 @@ export class ComponentViewerBase { filterTreeCommandDisposable, clearFilterCommandDisposable ); - vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true) + vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, true); return true; } @@ -237,7 +238,7 @@ export class ComponentViewerBase { } this._filterDebounceTimer = setTimeout(() => { this._filterDebounceTimer = undefined; - this.saveCurrentState(); + void this.saveCurrentState(); }, ComponentViewerBase.filterDebounceMs); }; @@ -274,7 +275,7 @@ export class ComponentViewerBase { clearTimeout(this._filterDebounceTimer); this._filterDebounceTimer = undefined; } - this.saveCurrentState(); + void this.saveCurrentState(); } protected async readScvdFiles(tracker: GDBTargetDebugTracker, session?: GDBTargetDebugSession): Promise { diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index f7ee45a9..cd0d63ce 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -65,11 +65,6 @@ jest.mock('../../../dynamic-view-states', () => ({ writeComponentViewerState: jest.fn().mockResolvedValue(undefined), })); -jest.mock('../../../dynamic-view-states', () => ({ - readComponentViewerState: jest.fn(), - writeComponentViewerState: jest.fn().mockResolvedValue(undefined), -})); - function asMockedFunction( fn: (...args: Args) => Return ): jest.MockedFunction<(...args: Args) => Return> { diff --git a/src/views/dynamic-view-states.test.ts b/src/views/dynamic-view-states.test.ts index dba18aab..5e3a6a96 100644 --- a/src/views/dynamic-view-states.test.ts +++ b/src/views/dynamic-view-states.test.ts @@ -265,4 +265,4 @@ describe('dynamic-view-states', () => { ); }); }); -}) +}); From 75c47df671c461323d65187a60696bc615775a4d Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 20 May 2026 13:32:05 +0200 Subject: [PATCH 15/17] Fix eslint error --- src/views/component-viewer/component-viewer-base.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index 685c098e..6a0bbaa5 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -112,7 +112,6 @@ export class ComponentViewerBase { await this.saveCurrentState(); await vscode.commands.executeCommand('setContext', `${this._viewId}.periodicUpdateEnabled`, false); componentViewerLogger.info(`${this._viewName}: Auto refresh disabled`); - }); const expandAllCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.expandAll`, async () => { componentViewerLogger.debug(`${this._viewName}: Expand all tree items`); From 9d94be8ee0f0005e2ec7ce171c5b3404afd80710 Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 20 May 2026 13:50:31 +0200 Subject: [PATCH 16/17] Add suggestions by Copilot --- src/features/cpu-states/cpu-states.ts | 6 +++--- .../test/unit/component-viewer-base.test.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 211e3bc0..05cc5f40 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -72,7 +72,7 @@ export class CpuStates { tracker.onContinued(event => this.handleContinuedEvent(event)); tracker.onStopped(event => this.handleStoppedEvent(event)); tracker.onStackTrace(stackTrace => this.handleStackTrace(stackTrace)); - vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); + void vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); } protected handleOnWillStartSession(session: GDBTargetDebugSession): void { @@ -101,7 +101,7 @@ export class CpuStates { this.activeSession = session; // Restore enabled/disabled state of CPU Time commands based on persisted settings const enabled = session ? (this.sessionCpuStates.get(session.session.id)?.enableCpuStatesFlag ?? true) : false; - vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', enabled); + void vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', enabled); this._onRefresh.fire(0); } @@ -369,7 +369,7 @@ export class CpuStates { for (const states of this.sessionCpuStates.values()) { states.enableCpuStatesFlag = true; } - vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); + void vscode.commands.executeCommand('setContext', 'vscode-cmsis-debugger.cpuTimerEnabled', true); logger.info('CPU States: CPU Timer reset'); } }; diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index cd0d63ce..be8777ee 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -129,7 +129,6 @@ describe('ComponentViewerBase', () => { tracker = trackerFactory(); controller = createController(context, provider); asMockedFunction(readComponentViewerState).mockReturnValue(undefined); - asMockedFunction(readComponentViewerState).mockReturnValue(undefined); // Extend registered commands for test class. const defaultMockedCommands = await vscode.commands.getCommands(); asMockedFunction(vscode.commands.getCommands).mockResolvedValue([ From 23f858650690b9cff73f1fb7696aa145bdc2581c Mon Sep 17 00:00:00 2001 From: Jen-Tse Huang Date: Wed, 20 May 2026 14:05:42 +0200 Subject: [PATCH 17/17] Suppress view state object injection lint --- src/views/dynamic-view-states.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/dynamic-view-states.ts b/src/views/dynamic-view-states.ts index bdd557ed..93cbaedd 100644 --- a/src/views/dynamic-view-states.ts +++ b/src/views/dynamic-view-states.ts @@ -25,6 +25,7 @@ type ViewStateEntry = { }; type ViewStateByConfigKey = Record>; +/* eslint-disable security/detect-object-injection */ function readDynamicViewState(configStateKey: string, dynamicView: T, mode: 'global' | 'merged' = 'merged'): ViewStateEntry[T] | undefined { const inspection = vscode.workspace.getConfiguration().inspect(SETTINGS_KEY); const globalEntry = inspection?.globalValue?.[configStateKey]; @@ -60,6 +61,7 @@ async function writeWorkspaceDynamicViewState(co const valueToStore = Object.keys(entriesToStore).length === 0 ? undefined : entriesToStore; await vscode.workspace.getConfiguration().update(SETTINGS_KEY, valueToStore, vscode.ConfigurationTarget.Workspace); } +/* eslint-enable security/detect-object-injection */ export async function clearAllViewState(): Promise { await Promise.all([