diff --git a/media/options-widget.css b/media/options-widget.css index 73538a7..fe7307c 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -120,7 +120,8 @@ gap: 8px; } -.advanced-options-dropdown { +.advanced-options-dropdown, +.advanced-options-input { width: 100%; } diff --git a/package.json b/package.json index d9d0453..7fb980d 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ { "command": "memory-inspector.show", "title": "Show Memory Inspector", + "icon": "$(file-binary)", "category": "Memory" }, { @@ -201,6 +202,13 @@ "group": "display@6", "when": "webviewId === memory-inspector.memory" } + ], + "editor/title": [ + { + "command": "memory-inspector.show", + "group": "navigation", + "when": "memory-inspector.canRead && (resourceLangId === c || resourceLangId === cpp)" + } ] }, "customEditors": [ @@ -372,6 +380,20 @@ "type": "boolean", "default": true, "description": "Display the radix prefix (e.g., '0x' for hexadecimal, '0b' for binary) before memory addresses." + }, + "memory-inspector.autoRefresh.enabled": { + "type": "boolean", + "default": false, + "description": "Enable Auto Refresh" + }, + "memory-inspector.autoRefresh.refreshRate": { + "type": [ + "number", + "undefined" + ], + "default": 500, + "minimum": 500, + "description": "The interval between auto-refresh calls. Leave empty to disable auto-refresh." } } } diff --git a/src/plugin/manifest.ts b/src/plugin/manifest.ts index e27a330..d53d9cf 100644 --- a/src/plugin/manifest.ts +++ b/src/plugin/manifest.ts @@ -51,6 +51,12 @@ export const DEFAULT_GROUPS_PER_ROW: GroupsPerRowOption = 4; export const CONFIG_ENDIANNESS = 'endianness'; export const DEFAULT_ENDIANNESS = Endianness.Little; +// Auto Refresh +export const CONFIG_AUTO_REFRESH_ENABLED = 'autoRefresh.enabled'; +export const DEFAULT_AUTO_REFRESH_ENABLED = false; +export const CONFIG_AUTO_REFRESH_RATE = 'autoRefresh.refreshRate'; +export const DEFAULT_AUTO_REFRESH_RATE = 500; + // Scroll export const CONFIG_SCROLLING_BEHAVIOR = 'scrollingBehavior'; export const DEFAULT_SCROLLING_BEHAVIOR = 'Paginate'; diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index ec585df..7e49835 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -205,8 +205,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { const disposables = [ this.messenger.onNotification(readyType, () => { - this.setInitialSettings(participant, panel.title); this.setSessionContext(participant, this.memoryProvider.createContext()); + this.setInitialSettings(participant, panel.title); this.refresh(participant, options); }, { sender: participant }), this.messenger.onRequest(setOptionsType, o => { @@ -266,9 +266,12 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { const addressPadding = AddressPaddingOptions[memoryInspectorConfiguration.get(manifest.CONFIG_ADDRESS_PADDING, manifest.DEFAULT_ADDRESS_PADDING)]; const addressRadix = memoryInspectorConfiguration.get(manifest.CONFIG_ADDRESS_RADIX, manifest.DEFAULT_ADDRESS_RADIX); const showRadixPrefix = memoryInspectorConfiguration.get(manifest.CONFIG_SHOW_RADIX_PREFIX, manifest.DEFAULT_SHOW_RADIX_PREFIX); + const autoRefreshEnabled = memoryInspectorConfiguration.get(manifest.CONFIG_AUTO_REFRESH_ENABLED, manifest.DEFAULT_AUTO_REFRESH_ENABLED); + const autoRefreshRate = memoryInspectorConfiguration.get(manifest.CONFIG_AUTO_REFRESH_RATE, manifest.DEFAULT_AUTO_REFRESH_RATE); return { messageParticipant, title, bytesPerWord, wordsPerGroup, groupsPerRow, - endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix + endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix, + autoRefreshRate, autoRefreshEnabled }; } diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index 96a5b71..f8834e9 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -90,6 +90,8 @@ export class MemoryWidget extends React.Component; protected extendedOptions = React.createRef(); protected labelEditInput = React.createRef(); + protected refreshRateInput = React.createRef(); protected coreOptionsDiv = React.createRef(); protected optionsMenuContext = createSectionVscodeContext('optionsWidget'); protected advancedOptionsContext = createSectionVscodeContext('advancedOptionsOverlay'); @@ -100,7 +105,7 @@ export class OptionsWidget extends React.Component { @@ -125,6 +130,11 @@ export class OptionsWidget extends React.Component + +

Auto-Refresh

+
+ + + +
@@ -487,6 +520,10 @@ export class OptionsWidget extends React.Component | React.KeyboardEvent) => void = event => this.doHandleRefreshRateChange(event); + doHandleRefreshRateChange(event: React.FocusEvent | React.KeyboardEvent): void { + if (!('key' in event) || event.key === 'Enter') { + const autoRefreshRate = tryToNumber(event.currentTarget.value); + this.props.updateRenderOptions({ autoRefreshRate }); + } + } + protected handleResetAdvancedOptions: MouseEventHandler | undefined = () => this.props.resetRenderOptions(); protected enableTitleEditing = () => this.doEnableTitleEditing(); diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index c84c8e2..18ce31b 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -80,6 +80,8 @@ const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = { addressPadding: 'Min', addressRadix: 16, showRadixPrefix: true, + autoRefreshEnabled: false, + autoRefreshRate: undefined }; const DEFAULT_READ_ARGUMENTS: Required = { @@ -90,6 +92,7 @@ const DEFAULT_READ_ARGUMENTS: Required = { class App extends React.Component<{}, MemoryAppState> { protected memoryWidget = React.createRef(); + protected refreshTimer?: NodeJS.Timeout | number; public constructor(props: {}) { super(props); @@ -135,6 +138,7 @@ class App extends React.Component<{}, MemoryAppState> { messenger.onRequest(getWebviewSelectionType, () => this.getWebviewSelection()); messenger.onNotification(showAdvancedOptionsType, () => this.showAdvancedOptions()); messenger.sendNotification(readyType, HOST_EXTENSION, undefined); + this.updateAutoRefresh(); } public componentDidUpdate(_: {}, prevState: MemoryAppState): void { @@ -147,9 +151,27 @@ class App extends React.Component<{}, MemoryAppState> { this.setState({ effectiveAddressLength }); } } + if (this.state.autoRefreshEnabled !== prevState.autoRefreshEnabled || this.state.autoRefreshRate !== prevState.autoRefreshRate) { + this.updateAutoRefresh(); + } hoverService.setMemoryState(this.state); } + componentWillUnmount(): void { + clearTimeout(this.refreshTimer); + } + + protected updateAutoRefresh = (): void => { + clearTimeout(this.refreshTimer); + + if (this.state.autoRefreshEnabled && this.state.autoRefreshRate && this.state.autoRefreshRate > 0) { + // we do not use an interval here as we only want to schedule another refresh AFTER the previous execution AND the delay has passed + // and not strictly every n milliseconds. Even if 'fetchMemory' fails here, we schedule another auto-refresh. + const scheduleRefresh = () => this.fetchMemory().finally(() => this.updateAutoRefresh()); + this.refreshTimer = setTimeout(scheduleRefresh, this.state.autoRefreshRate); + } + }; + // use a slight debounce as the same event may come in short succession protected memoryWritten = debounce((writtenMemory: WrittenMemory): void => { if (!this.state.memory) { @@ -222,6 +244,8 @@ class App extends React.Component<{}, MemoryAppState> { showRadixPrefix={this.state.showRadixPrefix} storeMemory={this.storeMemory} applyMemory={this.applyMemory} + autoRefreshEnabled={this.state.autoRefreshEnabled} + autoRefreshRate={this.state.autoRefreshRate} /> ; } @@ -246,16 +270,19 @@ class App extends React.Component<{}, MemoryAppState> { protected fetchMemory = async (partialOptions?: MemoryOptions): Promise => this.doFetchMemory(partialOptions); protected async doFetchMemory(partialOptions?: MemoryOptions): Promise { - if (this.state.isFrozen) { + if (this.state.isFrozen || !this.state.sessionContext.canRead) { return; } - this.setState(prev => ({ ...prev, isMemoryFetching: true })); const completeOptions = { memoryReference: partialOptions?.memoryReference || this.state.activeReadArguments.memoryReference, offset: partialOptions?.offset ?? this.state.activeReadArguments.offset, count: partialOptions?.count ?? this.state.activeReadArguments.count }; - + if (completeOptions.memoryReference === '') { + // may happen when we initialize empty + return; + } + this.setState(prev => ({ ...prev, isMemoryFetching: true })); try { const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions); await Promise.all(Array.from( diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 8b0c6ac..82a2509 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -89,6 +89,8 @@ export interface MemoryDisplayConfiguration { addressPadding: AddressPadding; addressRadix: Radix; showRadixPrefix: boolean; + autoRefreshEnabled: boolean; + autoRefreshRate?: number; } export type ScrollingBehavior = 'Paginate' | 'Grow' | 'Auto-Append';