Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Use React context instead of prop drilling #85

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions src/webview/components/memory-app-provider.tsx
Original file line number Diff line number Diff line change
@@ -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<MemoryAppContext>(MEMORY_APP_CONTEXT_DEFAULTS);

export interface MemoryAppContext extends MemoryAppState {
updateMemoryState: (newState: Partial<MemoryAppState>) => void;
updateMemoryDisplayConfiguration: (newState: Partial<MemoryDisplayConfiguration>) => void;
resetMemoryDisplayConfiguration: () => void;
updateTitle: (title: string) => void;
refreshMemory: () => void;
fetchMemory: (partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>) => Promise<void>;
toggleColumn: (id: string, active: boolean) => void;
}

interface MemoryAppProviderProps {
children: React.ReactNode;
}

export class MemoryAppProvider extends React.Component<MemoryAppProviderProps, MemoryAppState> {

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 (
<MemoryAppContext.Provider value={contextValue}>
{this.props.children}
</MemoryAppContext.Provider>
);
}

protected updateMemoryState = (newState: Partial<MemoryState>) => this.setState(prevState => ({ ...prevState, ...newState }));
protected updateMemoryDisplayConfiguration = (newState: Partial<MemoryDisplayConfiguration>) => 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<DebugProtocol.ReadMemoryArguments>): Promise<void> {
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<DebugProtocol.ReadMemoryArguments>): Promise<void> => this.doFetchMemory(partialOptions);
protected async doFetchMemory(partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void> {
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<void> {
const columns = isVisible ? await columnContributionService.show(id, this.state) : columnContributionService.hide(id);
this.setState(prevState => ({ ...prevState, columns }));
}
}
67 changes: 36 additions & 31 deletions src/webview/components/memory-table.tsx
Original file line number Diff line number Diff line change
@@ -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<MoreMemorySelectProps> = ({ count, offse
);
};

interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguration {
memory?: Memory;
decorations: Decoration[];
offset: number;
count: number;
fetchMemory(partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void>;
isMemoryFetching: boolean;
interface MemoryTableProps {
endianness: Endianness;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to your changes, but it seems we don't really ever use the endianness values we pass around.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed the same. We have "Selectable word endianess" in #48 . But I was falsely assuming before that the word display was little endian already which seems to be the internal default. I've raised #88

columnOptions: ColumnStatus[];
}

interface MemoryRowListOptions {
@@ -118,10 +115,10 @@ interface MemoryTableState {
selection: DataTableCellSelection<MemoryRowData[]> | null;
}

type MemorySizeOptions = Pick<MemoryTableProps, 'bytesPerWord' | 'wordsPerGroup' | 'groupsPerRow'>;
type MemorySizeOptions = Pick<MemoryDisplayConfiguration, 'bytesPerWord' | 'wordsPerGroup' | 'groupsPerRow'>;
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<MemoryTableProps, MemoryTableState> {
static contextType = MemoryAppContext;
declare context: MemoryAppContext;

protected prevContext?: MemoryAppContext;

protected datatableRef = React.createRef<DataTable<MemoryRowData[]>>();

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<MemoryTableProps, MemoryTab
};
}

componentDidUpdate(prevProps: Readonly<MemoryTableProps>): 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,15 +190,15 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
>
{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 <Column
key={contribution.id}
field={contribution.id}
header={contribution.label}
className={classNames({ fit })}
headerClassName={classNames({ fit })}
style={{ width: fit ? undefined : `${columnWidth}%` }}
body={(row?: MemoryRowData) => row && contribution.render(row, this.props.memory!, this.props)}>
body={(row?: MemoryRowData) => row && contribution.render(row, this.context.memory!, renderOptions)}>
{contribution.label}
</Column>;
})}
@@ -228,7 +233,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
};

protected renderHeader(): React.ReactNode | undefined {
const { offset, count, fetchMemory, scrollingBehavior } = this.props;
const { offset, count, fetchMemory, scrollingBehavior } = this.context;

let memorySelect: React.ReactNode | undefined;
let loading: React.ReactNode | undefined;
@@ -246,7 +251,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
</div>;
}

if (this.props.isMemoryFetching) {
if (this.context.isMemoryFetching) {
loading = <div className='absolute right-0 flex align-items-center'>
<ProgressSpinner style={{ width: '16px', height: '16px' }} className='mr-2' />
<span>Loading</span>
@@ -262,7 +267,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
}

protected renderFooter(): React.ReactNode | undefined {
const { offset, count, fetchMemory, scrollingBehavior } = this.props;
const { offset, count, fetchMemory, scrollingBehavior } = this.context;

let memorySelect: React.ReactNode | undefined;

Loading