From cbd99215d0b45ec3521f07ef429acaf9dae85152 Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Tue, 27 Feb 2024 09:32:46 +0100 Subject: [PATCH] Proposal: Use React context instead of prop drilling for accessing shared state - Refactor app component logic into a context provider - Wrap the root component into the newly created `MemoryAppProvider`. This way child components can access the central state via context API and props drilling is no longer required. - Refactor child components to access the central state info via `context` instead of props. --- .../components/memory-app-provider.tsx | 199 ++++++++++++++++++ src/webview/components/memory-table.tsx | 67 +++--- src/webview/components/memory-widget.tsx | 64 +----- src/webview/components/options-widget.tsx | 83 ++++---- src/webview/memory-webview-view.tsx | 172 +-------------- 5 files changed, 294 insertions(+), 291 deletions(-) create mode 100644 src/webview/components/memory-app-provider.tsx diff --git a/src/webview/components/memory-app-provider.tsx b/src/webview/components/memory-app-provider.tsx new file mode 100644 index 0000000..cd087b6 --- /dev/null +++ b/src/webview/components/memory-app-provider.tsx @@ -0,0 +1,199 @@ +/******************************************************************************** + * Copyright (C) 2022 Ericsson, Arm and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugProtocol } from '@vscode/debugprotocol'; +import React from 'react'; +import { HOST_EXTENSION } from 'vscode-messenger-common'; +import { logMessageType, readMemoryType, readyType, resetMemoryViewSettingsType, setMemoryViewSettingsType, setOptionsType, setTitleType } from '../../common/messaging'; +import { AddressColumn } from '../columns/address-column'; +import { AsciiColumn } from '../columns/ascii-column'; +import { ColumnStatus, columnContributionService } from '../columns/column-contribution-service'; +import { DataColumn } from '../columns/data-column'; +import { decorationService } from '../decorations/decoration-service'; +import { Decoration, Memory, MemoryDisplayConfiguration, MemoryState } from '../utils/view-types'; +import { variableDecorator } from '../variables/variable-decorations'; +import { messenger } from '../view-messenger'; + +export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration { + title: string; + decorations: Decoration[]; + columns: ColumnStatus[]; + offset: number; +} + +const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = { + bytesPerWord: 1, + wordsPerGroup: 1, + groupsPerRow: 4, + scrollingBehavior: 'Paginate', + addressRadix: 16, + showRadixPrefix: true, +}; + +const MEMORY_APP_STATE_DEFAULTS: MemoryAppState = { + title: 'Memory', + memory: undefined, + memoryReference: '', + offset: 0, + count: 256, + decorations: [], + columns: columnContributionService.getColumns(), + isMemoryFetching: false, + ...MEMORY_DISPLAY_CONFIGURATION_DEFAULTS +}; + +const MEMORY_APP_CONTEXT_DEFAULTS: MemoryAppContext = { + ...MEMORY_APP_STATE_DEFAULTS, + updateMemoryState: () => { }, + updateMemoryDisplayConfiguration: () => { }, + resetMemoryDisplayConfiguration: () => { }, + updateTitle: () => { }, + refreshMemory: () => { }, + fetchMemory: async () => { }, + toggleColumn: () => { } +}; + +export const MemoryAppContext = React.createContext(MEMORY_APP_CONTEXT_DEFAULTS); + +export interface MemoryAppContext extends MemoryAppState { + updateMemoryState: (newState: Partial) => void; + updateMemoryDisplayConfiguration: (newState: Partial) => void; + resetMemoryDisplayConfiguration: () => void; + updateTitle: (title: string) => void; + refreshMemory: () => void; + fetchMemory: (partialOptions?: Partial) => Promise; + toggleColumn: (id: string, active: boolean) => void; +} + +interface MemoryAppProviderProps { + children: React.ReactNode; +} + +export class MemoryAppProvider extends React.Component { + + public constructor(props: MemoryAppProviderProps) { + super(props); + columnContributionService.register(new AddressColumn(), false); + columnContributionService.register(new DataColumn(), false); + columnContributionService.register(variableDecorator); + columnContributionService.register(new AsciiColumn()); + decorationService.register(variableDecorator); + this.state = { + title: 'Memory', + memory: undefined, + memoryReference: '', + offset: 0, + count: 256, + decorations: [], + columns: columnContributionService.getColumns(), + isMemoryFetching: false, + ...MEMORY_DISPLAY_CONFIGURATION_DEFAULTS + }; + } + + public componentDidMount(): void { + messenger.onRequest(setOptionsType, options => this.setOptions(options)); + messenger.onNotification(setMemoryViewSettingsType, config => { + for (const column of columnContributionService.getColumns()) { + const id = column.contribution.id; + const configurable = column.configurable; + this.toggleColumn(id, !configurable || !!config.visibleColumns?.includes(id)); + } + this.setState(prevState => ({ ...prevState, ...config, title: config.title ?? prevState.title, })); + }); + messenger.sendNotification(readyType, HOST_EXTENSION, undefined); + } + + public render(): React.ReactNode { + const contextValue: MemoryAppContext = { + ...this.state, + updateMemoryState: this.updateMemoryState, + fetchMemory: this.fetchMemory, + refreshMemory: this.refreshMemory, + resetMemoryDisplayConfiguration: this.resetMemoryDisplayConfiguration, + toggleColumn: this.toggleColumn, + updateMemoryDisplayConfiguration: this.updateMemoryDisplayConfiguration, + updateTitle: this.updateTitle + }; + + return ( + + {this.props.children} + + ); + } + + protected updateMemoryState = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); + protected updateMemoryDisplayConfiguration = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); + protected resetMemoryDisplayConfiguration = () => messenger.sendNotification(resetMemoryViewSettingsType, HOST_EXTENSION, undefined); + protected updateTitle = (title: string) => { + this.setState({ title }); + messenger.sendNotification(setTitleType, HOST_EXTENSION, title); + }; + + protected async setOptions(options?: Partial): Promise { + messenger.sendRequest(logMessageType, HOST_EXTENSION, `Setting options: ${JSON.stringify(options)}`); + this.setState(prevState => ({ ...prevState, ...options })); + return this.fetchMemory(options); + } + + protected refreshMemory = () => { this.fetchMemory(); }; + + protected fetchMemory = async (partialOptions?: Partial): Promise => this.doFetchMemory(partialOptions); + protected async doFetchMemory(partialOptions?: Partial): Promise { + this.setState(prev => ({ ...prev, isMemoryFetching: true })); + const completeOptions = { + memoryReference: partialOptions?.memoryReference || this.state.memoryReference, + offset: partialOptions?.offset ?? this.state.offset, + count: partialOptions?.count ?? this.state.count + }; + + try { + const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions); + await Promise.all(Array.from( + new Set(columnContributionService.getUpdateExecutors().concat(decorationService.getUpdateExecutors())), + executor => executor.fetchData(completeOptions) + )); + + this.setState({ + decorations: decorationService.decorations, + memory: this.convertMemory(response), + memoryReference: completeOptions.memoryReference, + offset: completeOptions.offset, + count: completeOptions.count, + isMemoryFetching: false + }); + + messenger.sendRequest(setOptionsType, HOST_EXTENSION, completeOptions); + } finally { + this.setState(prev => ({ ...prev, isMemoryFetching: false })); + } + + } + + protected convertMemory(result: DebugProtocol.ReadMemoryResponse['body']): Memory { + if (!result?.data) { throw new Error('No memory provided!'); } + const address = BigInt(result.address); + const bytes = Uint8Array.from(Buffer.from(result.data, 'base64')); + return { bytes, address }; + } + + protected toggleColumn = (id: string, active: boolean): void => { this.doToggleColumn(id, active); }; + protected async doToggleColumn(id: string, isVisible: boolean): Promise { + const columns = isVisible ? await columnContributionService.show(id, this.state) : columnContributionService.hide(id); + this.setState(prevState => ({ ...prevState, columns })); + } +} diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index e0cb422..cc35929 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -15,16 +15,17 @@ ********************************************************************************/ import { DebugProtocol } from '@vscode/debugprotocol'; +import isDeepEqual from 'fast-deep-equal'; import memoize from 'memoize-one'; import { Column } from 'primereact/column'; import { DataTable, DataTableCellSelection, DataTableProps, DataTableRowData, DataTableSelectionCellChangeEvent } from 'primereact/datatable'; import { ProgressSpinner } from 'primereact/progressspinner'; +import { classNames } from 'primereact/utils'; import React from 'react'; -import { TableRenderOptions } from '../columns/column-contribution-service'; -import { Decoration, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types'; -import isDeepEqual from 'fast-deep-equal'; import { AddressColumn } from '../columns/address-column'; -import { classNames } from 'primereact/utils'; +import { ColumnStatus, TableRenderOptions } from '../columns/column-contribution-service'; +import { Endianness, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types'; +import { MemoryAppContext } from './memory-app-provider'; export interface MoreMemorySelectProps { count: number; @@ -93,13 +94,9 @@ export const MoreMemorySelect: React.FC = ({ count, offse ); }; -interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguration { - memory?: Memory; - decorations: Decoration[]; - offset: number; - count: number; - fetchMemory(partialOptions?: Partial): Promise; - isMemoryFetching: boolean; +interface MemoryTableProps { + endianness: Endianness; + columnOptions: ColumnStatus[]; } interface MemoryRowListOptions { @@ -118,10 +115,10 @@ interface MemoryTableState { selection: DataTableCellSelection | null; } -type MemorySizeOptions = Pick; +type MemorySizeOptions = Pick; namespace MemorySizeOptions { - export function create(props: MemoryTableProps): MemorySizeOptions { - const { groupsPerRow, bytesPerWord, wordsPerGroup }: MemorySizeOptions = props; + export function create(context: MemoryAppContext): MemorySizeOptions { + const { groupsPerRow, bytesPerWord, wordsPerGroup }: MemorySizeOptions = context; return { bytesPerWord, groupsPerRow, @@ -131,11 +128,15 @@ namespace MemorySizeOptions { } export class MemoryTable extends React.PureComponent { + static contextType = MemoryAppContext; + declare context: MemoryAppContext; + + protected prevContext?: MemoryAppContext; protected datatableRef = React.createRef>(); protected get isShowMoreEnabled(): boolean { - return !!this.props.memory?.bytes.length; + return !!this.context.memory?.bytes.length; } constructor(props: MemoryTableProps) { @@ -151,24 +152,28 @@ 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 hasOptionsChanged = prevProps.wordsPerGroup !== this.props.wordsPerGroup || prevProps.groupsPerRow !== this.props.groupsPerRow; - - // Reset selection - const selection = this.state.selection; - if (selection && (hasMemoryChanged || hasOptionsChanged)) { - // eslint-disable-next-line no-null/no-null - this.setState(prev => ({ ...prev, selection: null })); + componentDidUpdate(): void { + if (this.prevContext && this.prevContext !== this.context) { + const hasMemoryChanged = this.prevContext.memory?.address !== this.context.memory?.address + || this.prevContext.offset !== this.context.offset || this.prevContext.count !== this.context.count; + const hasOptionsChanged = this.prevContext.wordsPerGroup !== this.context.wordsPerGroup || this.prevContext.groupsPerRow !== this.context.groupsPerRow; + + // Reset selection + const selection = this.state.selection; + if (selection && (hasMemoryChanged || hasOptionsChanged)) { + // eslint-disable-next-line no-null/no-null + this.setState(prev => ({ ...prev, selection: null })); + } } + this.prevContext = this.context; } public render(): React.ReactNode { - const memory = this.props.memory; + const memory = this.context.memory; let rows: MemoryRowData[] = []; if (memory) { - const memorySizeOptions = MemorySizeOptions.create(this.props); + const memorySizeOptions = MemorySizeOptions.create(this.context); const options = this.createMemoryRowListOptions(memory, memorySizeOptions); rows = this.createTableRows(memory, options); } @@ -185,7 +190,7 @@ export class MemoryTable extends React.PureComponent {this.props.columnOptions.map(({ contribution }) => { const fit = contribution.id === AddressColumn.ID; - + const renderOptions: TableRenderOptions = { columnOptions: this.props.columnOptions, endianness: this.props.endianness, ...this.context }; return row && contribution.render(row, this.props.memory!, this.props)}> + body={(row?: MemoryRowData) => row && contribution.render(row, this.context.memory!, renderOptions)}> {contribution.label} ; })} @@ -228,7 +233,7 @@ export class MemoryTable extends React.PureComponent; } - if (this.props.isMemoryFetching) { + if (this.context.isMemoryFetching) { loading =
Loading @@ -262,7 +267,7 @@ export class MemoryTable extends React.PureComponent void; - updateMemoryArguments: (memoryArguments: Partial) => void; - toggleColumn(id: string, active: boolean): void; - updateMemoryDisplayConfiguration: (memoryArguments: Partial) => void; - resetMemoryDisplayConfiguration: () => void; - updateTitle: (title: string) => void; - fetchMemory(partialOptions?: Partial): Promise -} - interface MemoryWidgetState { endianness: Endianness; } @@ -47,48 +28,21 @@ const defaultOptions: MemoryWidgetState = { endianness: Endianness.Little, }; -export class MemoryWidget extends React.Component { - constructor(props: MemoryWidgetProps) { +export class MemoryWidget extends React.Component<{}, MemoryWidgetState> { + static contextType = MemoryAppContext; + declare context: MemoryAppContext; + + constructor(props: {}) { super(props); this.state = { ...defaultOptions }; } override render(): React.ReactNode { return (
- + candidate.active)} - memory={this.props.memory} endianness={this.state.endianness} - bytesPerWord={this.props.bytesPerWord} - wordsPerGroup={this.props.wordsPerGroup} - groupsPerRow={this.props.groupsPerRow} - offset={this.props.offset} - count={this.props.count} - fetchMemory={this.props.fetchMemory} - isMemoryFetching={this.props.isMemoryFetching} - scrollingBehavior={this.props.scrollingBehavior} - addressRadix={this.props.addressRadix} - showRadixPrefix={this.props.showRadixPrefix} + columnOptions={this.context.columns.filter(candidate => candidate.active)} />
); } diff --git a/src/webview/components/options-widget.tsx b/src/webview/components/options-widget.tsx index 46e67ef..876878f 100644 --- a/src/webview/components/options-widget.tsx +++ b/src/webview/components/options-widget.tsx @@ -14,33 +14,22 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import type { DebugProtocol } from '@vscode/debugprotocol'; import { Formik, FormikConfig, FormikErrors, FormikProps } from 'formik'; import { Button } from 'primereact/button'; +import { Checkbox } from 'primereact/checkbox'; import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown'; import { InputText } from 'primereact/inputtext'; import { OverlayPanel } from 'primereact/overlaypanel'; import { classNames } from 'primereact/utils'; import React, { FocusEventHandler, KeyboardEvent, KeyboardEventHandler, MouseEventHandler } from 'react'; -import { TableRenderOptions } from '../columns/column-contribution-service'; import { - SerializedTableRenderOptions, + Endianness } from '../utils/view-types'; +import { MemoryAppContext } from './memory-app-provider'; import { MultiSelectWithLabel } from './multi-select'; -import { Checkbox } from 'primereact/checkbox'; -export interface OptionsWidgetProps - extends Omit, - Required { - title: string; - updateRenderOptions: (options: Partial) => void; - resetRenderOptions: () => void; - updateTitle: (title: string) => void; - updateMemoryArguments: ( - memoryArguments: Partial - ) => void; - refreshMemory: () => void; - toggleColumn(id: string, isVisible: boolean): void; +export interface OptionsWidgetProps { + endianness: Endianness; } interface OptionsWidgetState { @@ -69,30 +58,36 @@ const allowedWordsPerGroup = [1, 2, 4, 8, 16]; const allowedGroupsPerRow = [1, 2, 4, 8, 16, 32]; export class OptionsWidget extends React.Component { + static contextType = MemoryAppContext; + declare context: MemoryAppContext; + protected formConfig: FormikConfig; protected extendedOptions = React.createRef(); protected labelEditInput = React.createRef(); protected get optionsFormValues(): OptionsForm { return { - address: this.props.memoryReference, - offset: this.props.offset.toString(), - count: this.props.count.toString(), + address: this.context.memoryReference, + offset: this.context.offset.toString(), + count: this.context.count.toString(), }; } constructor(props: OptionsWidgetProps) { super(props); - + this.state = { isTitleEditing: false }; this.formConfig = { - initialValues: this.optionsFormValues, + initialValues: { + address: '', + offset: '', + count: '', + }, enableReinitialize: true, validate: this.validate, onSubmit: () => { - this.props.refreshMemory(); + this.context.refreshMemory(); }, }; - this.state = { isTitleEditing: false }; } protected validate = (values: OptionsForm) => { @@ -152,7 +147,7 @@ export class OptionsWidget extends React.Component {!isLabelEditing && ( -

{this.props.title}

+

{this.context.title}

)} {!isLabelEditing && (