diff --git a/media/options-widget.css b/media/options-widget.css index 23ebf4e..5275d33 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -74,6 +74,11 @@ row-gap: 12px; } +.form-textfield { + position: relative; + padding-bottom: 16px; +} + .form-textfield > input { width: 150px; } @@ -89,6 +94,12 @@ align-self: start; } +.form-options-memory-read-argument-hint { + position: absolute; + bottom: 0; + color: var(--vscode-descriptionForeground); +} + .advanced-options-content h2 { font-size: 120%; margin: 0.5rem 0 0 0; diff --git a/package.json b/package.json index 588366d..11cdfef 100644 --- a/package.json +++ b/package.json @@ -214,12 +214,14 @@ "type": "string", "enum": [ "Paginate", - "Infinite" + "Grow", + "Auto-Append" ], "default": "Paginate", "enumDescriptions": [ "Maintains a consistent memory size, replacing the previous request.", - "Appends new memory to bounds of current request, resulting in a growing list." + "Appends new memory to bounds of current request, resulting in a growing list.", + "Automatically appends new memory to the bounds of the current request on reaching the end of the list, resulting in a growing list." ], "description": "Behavior when adding more memory beyond the current view." }, diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index 76465ad..6340bc9 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -28,17 +28,25 @@ import { tryToNumber } from '../../common/typescript'; import { DataColumn } from '../columns/data-column'; export interface MoreMemorySelectProps { - count: number; - offset: number; + activeReadArguments: Required; options: number[]; direction: 'above' | 'below'; - scrollingBehavior: ScrollingBehavior; fetchMemory(partialOptions?: Partial): Promise; disabled: boolean } -export const MoreMemorySelect: React.FC = ({ count, offset, options, fetchMemory, direction, scrollingBehavior, disabled }) => { - const [numBytes, setNumBytes] = React.useState(options[0]); +export interface MoreMemoryAboveSelectProps extends MoreMemorySelectProps { + direction: 'above'; + shouldPrepend?: boolean; +} + +export interface MoreMemoryBelowSelectProps extends MoreMemorySelectProps { + direction: 'below'; + shouldAppend?: boolean; +} + +export const MoreMemorySelect: React.FC = props => { + const [numBytes, setNumBytes] = React.useState(props.options[0]); const containerRef = React.createRef(); const onSelectChange = (e: React.ChangeEvent): void => { e.stopPropagation(); @@ -46,43 +54,60 @@ export const MoreMemorySelect: React.FC = ({ count, offse setNumBytes(parseInt(value)); }; - const loadMoreMemory = (e: React.MouseEvent | React.KeyboardEvent): void => { + const updateMemory = (e: React.MouseEvent | React.KeyboardEvent): void => { containerRef.current?.blur(); if (isTrigger(e)) { - let newOffset = offset; - let newCount = count; + const direction = props.direction; + if (direction === 'above') { - newOffset = offset - numBytes; - } - if (scrollingBehavior === 'Infinite') { - newCount = count + numBytes; + handleAboveDirection(props); + } else if (direction === 'below') { + handleBelowDirection(props); } else { - if (direction === 'below') { - newOffset = offset + numBytes; - } + throw new Error(`Unknown direction ${direction}`); } - fetchMemory({ offset: newOffset, count: newCount }); + } + }; + + const handleAboveDirection = (aboveProps: MoreMemoryAboveSelectProps): void => { + const { activeReadArguments, shouldPrepend, fetchMemory } = aboveProps; + + const newOffset = activeReadArguments.offset - numBytes; + const newCount = shouldPrepend ? activeReadArguments.count + numBytes : activeReadArguments.count; + + fetchMemory({ offset: newOffset, count: newCount }); + }; + + const handleBelowDirection = (belowProps: MoreMemoryBelowSelectProps): void => { + const { activeReadArguments, fetchMemory } = belowProps; + + if (belowProps.shouldAppend) { + const newCount = activeReadArguments.count + numBytes; + fetchMemory({ count: newCount }); + } else { + const newOffset = activeReadArguments.offset + numBytes; + fetchMemory({ offset: newOffset }); } }; return (
Load - {`more bytes ${direction}`} + {`more bytes ${props.direction}`}
); }; interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguration { + configuredReadArguments: Required; + activeReadArguments: Required; memory?: Memory; decorations: Decoration[]; - offset: number; - count: number; effectiveAddressLength: number; fetchMemory(partialOptions?: Partial): Promise; isMemoryFetching: boolean; @@ -145,8 +170,12 @@ export class MemoryTable extends React.PureComponent>(); protected resizeObserver?: ResizeObserver; - protected get isShowMoreEnabled(): boolean { - return !!this.props.memory?.bytes.length; + protected get datatableWrapper(): HTMLElement | undefined { + return this.datatableRef.current?.getElement().querySelector('[data-pc-section="wrapper"]') ?? undefined; + } + + protected get isLoading(): boolean { + return this.props.isMemoryFetching; } constructor(props: MemoryTableProps) { @@ -167,6 +196,11 @@ export class MemoryTable extends React.PureComponent { if (entries.length > 0) { this.autofitColumns(); + + // The size changed - we could have too few rows visible to enable a scrollbar + if (this.props.scrollingBehavior === 'Auto-Append') { + this.ensureSufficientVisibleRowsForScrollbar(); + } } }); @@ -177,7 +211,11 @@ export class MemoryTable extends React.PureComponent): void { - const hasMemoryChanged = prevProps.memory?.address !== this.props.memory?.address || prevProps.offset !== this.props.offset || prevProps.count !== this.props.count; + const hasMemoryChanged = (prevProps.memory === undefined || this.props.memory === undefined) + || prevProps.memory.address !== this.props.memory.address + || prevProps.activeReadArguments.offset !== this.props.activeReadArguments.offset + || prevProps.activeReadArguments.count !== this.props.activeReadArguments.count; + const hasOptionsChanged = prevProps.wordsPerGroup !== this.props.wordsPerGroup || prevProps.groupsPerRow !== this.props.groupsPerRow; // Reset selection @@ -188,6 +226,22 @@ export class MemoryTable extends React.PureComponent this.props.activeReadArguments.count) { + this.datatableRef.current?.resetScroll(); + } + + // If we disable frozen, then we need to check the current position of the scrollbar and if necessary append more memory + if (prevProps.isFrozen && !this.props.isFrozen) { + const wrapper = this.datatableWrapper; + if (wrapper) { + this.appendMoreMemoryOnListEnd(wrapper); + } + } + } } componentWillUnmount(): void { @@ -204,7 +258,7 @@ export class MemoryTable extends React.PureComponent c.contribution.fittingType === 'content-width').length; @@ -242,7 +296,6 @@ export class MemoryTable extends React.PureComponent): DataTableProps { + if (this.props.scrollingBehavior === 'Auto-Append') { + return { + ...props, + pt: { + wrapper: { + onScroll: event => { + this.appendMoreMemoryOnListEnd(event.currentTarget); + } + } + } + }; + } else { + return { + ...props, + footer: this.renderFooter() + }; + } + } + + /** + * This method ensures that we have sufficient rows visible to enable vertical scrollbars + */ + protected ensureSufficientVisibleRowsForScrollbar(): void { + if (this.props.memory === undefined) { + return; + } + + const requestedBytesNotLoaded = this.props.activeReadArguments.count > this.props.memory.bytes.length; + if (requestedBytesNotLoaded) { + return; + } + + const datatableValues = this.datatableRef.current?.props.value; + if (datatableValues && datatableValues.length < MemoryTable.renderableRowsAtOnceCountForWrapper(this.datatableWrapper)) { + // We have too few rows, we need to load more data + this.appendMoreMemory(); + } + } + + protected appendMoreMemory(): void { + if (!this.isLoading && this.props.memory !== undefined) { + const memorySizeOptions = MemorySizeOptions.create(this.props, this.state); + const options = this.createMemoryRowListOptions(this.props.memory, memorySizeOptions); + const newCount = this.props.activeReadArguments.count + options.wordsPerRow * MemoryTable.renderableRowsAtOnceCountForWrapper(this.datatableWrapper); + this.props.fetchMemory({ count: newCount }); + } + } + + protected appendMoreMemoryOnListEnd(element: HTMLElement): void { + if (!this.isLoading) { + // Append new data only if we reach the bottom + const distanceBottom = Math.abs(element.scrollHeight - element.scrollTop - element.clientHeight); + + if (distanceBottom < 1) { + this.appendMoreMemory(); + } + } + } + protected onSelectionChanged = (event: DataTableSelectionCellChangeEvent) => { this.setState(prev => ({ ...prev, selection: event.value })); }; @@ -267,26 +380,24 @@ export class MemoryTable extends React.PureComponent ; } - if (this.props.isMemoryFetching) { + if (this.isLoading) { loading =
Loading @@ -302,19 +413,16 @@ export class MemoryTable extends React.PureComponent
; @@ -393,4 +501,27 @@ export class MemoryTable extends React.PureComponent('tr'); + + if (row) { + return Math.ceil(wrapper.clientHeight / row.clientHeight); + } + } + + return 1; + } + + /** + * Returns the number of rows that the wrapper can render at once + */ + export function renderableRowsAtOnceCountForWrapper(wrapper?: HTMLElement): number { + const buffer = 8; + return visibleRowsCountInWrapper(wrapper) + buffer; + } } diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index 179ae2d..367795c 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -17,22 +17,20 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import React from 'react'; import { ColumnStatus } from '../columns/column-contribution-service'; -import { Decoration, Endianness, Memory, MemoryDisplayConfiguration } from '../utils/view-types'; +import { Decoration, Endianness, Memory, MemoryDisplayConfiguration, MemoryState } from '../utils/view-types'; import { MemoryTable } from './memory-table'; import { OptionsWidget } from './options-widget'; interface MemoryWidgetProps extends MemoryDisplayConfiguration { + configuredReadArguments: Required; + activeReadArguments: Required; memory?: Memory; title: string; decorations: Decoration[]; columns: ColumnStatus[]; - memoryReference: string; - offset: number; - count: number; effectiveAddressLength: number; isMemoryFetching: boolean; - refreshMemory: () => void; - updateMemoryArguments: (memoryArguments: Partial) => void; + updateMemoryState: (state: Partial) => void; toggleColumn(id: string, active: boolean): void; isFrozen: boolean; toggleFrozen: () => void; @@ -62,25 +60,26 @@ export class MemoryWidget extends React.Component candidate.active)} memory={this.props.memory} @@ -88,8 +87,6 @@ export class MemoryWidget extends React.Component, - Required { + extends Omit { + configuredReadArguments: Required; + activeReadArguments: Required; title: string; updateRenderOptions: (options: Partial) => void; resetRenderOptions: () => void; updateTitle: (title: string) => void; - updateMemoryArguments: ( - memoryArguments: Partial - ) => void; - refreshMemory: () => void; + updateMemoryState: (state: Partial) => void; + fetchMemory(partialOptions?: Partial): Promise toggleColumn(id: string, isVisible: boolean): void; toggleFrozen: () => void; isFrozen: boolean; @@ -74,9 +73,9 @@ export class OptionsWidget extends React.Component { - this.props.refreshMemory(); + this.props.fetchMemory(this.props.configuredReadArguments); }, }; this.state = { isTitleEditing: false }; @@ -141,6 +140,11 @@ export class OptionsWidget extends React.Component { + if (userValue !== memoryValue) { + return Actual: {memoryValue}; + } + }; return (
@@ -182,7 +186,7 @@ export class OptionsWidget extends React.Component {formik => (
- + @@ -199,6 +203,7 @@ export class OptionsWidget extends React.Component) : undefined} + {activeMemoryReadArgumentHint(this.props.configuredReadArguments.memoryReference, this.props.activeReadArguments.memoryReference)}