diff --git a/package.json b/package.json index e8567a8..b252dd3 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,48 @@ "command": "memory-inspector.show-variable", "title": "Show in Memory Inspector", "category": "Memory" + }, + { + "command": "memory-inspector.show-variables-column", + "title": "Show Variables Column", + "category": "Memory", + "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.hide-variables-column", + "title": "Hide Variables Column", + "category": "Memory", + "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.show-ascii-column", + "title": "Show ASCII Column", + "category": "Memory", + "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.hide-ascii-column", + "title": "Hide ASCII Column", + "category": "Memory", + "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.show-radix-prefix", + "title": "Show Radix Prefix", + "category": "Memory", + "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.hide-radix-prefix", + "title": "Hide Radix Prefix", + "category": "Memory", + "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.show-advanced-display-options", + "title": "Advanced Display Options", + "category": "Memory", + "enablement": "webviewId === memory-inspector.memory" } ], "menus": { @@ -105,6 +147,47 @@ "command": "memory-inspector.show-variable", "when": "canViewMemory && memory-inspector.canRead" } + ], + "webview/context": [ + { + "command": "memory-inspector.show-variables-column", + "title": "Show Variables Column", + "group": "display@1", + "when": "webviewId === memory-inspector.memory && !(visibleColumns =~ /\\|variables\\|/)" + }, + { + "command": "memory-inspector.hide-variables-column", + "title": "Hide Variables Column", + "group": "display@1", + "when": "webviewId === memory-inspector.memory && visibleColumns =~ /\\|variables\\|/" + }, + { + "command": "memory-inspector.show-ascii-column", + "title": "Show ASCII Column", + "group": "display@2", + "when": "webviewId === memory-inspector.memory && !(visibleColumns =~ /\\|ascii\\|/)" + }, + { + "command": "memory-inspector.hide-ascii-column", + "title": "Hide ASCII Column", + "group": "display@2", + "when": "webviewId === memory-inspector.memory && visibleColumns =~ /\\|ascii\\|/" + }, + { + "command": "memory-inspector.show-radix-prefix", + "group": "display@3", + "when": "webviewId === memory-inspector.memory && !showRadixPrefix" + }, + { + "command": "memory-inspector.hide-radix-prefix", + "group": "display@3", + "when": "webviewId === memory-inspector.memory && showRadixPrefix" + }, + { + "command": "memory-inspector.show-advanced-display-options", + "group": "display@4", + "when": "webviewId === memory-inspector.memory" + } ] }, "customEditors": [ diff --git a/src/common/messaging.ts b/src/common/messaging.ts index a8d9d8d..2eaa96c 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -31,3 +31,14 @@ export const setOptionsType: RequestType = { method: 'readMemory' }; export const writeMemoryType: RequestType = { method: 'writeMemory' }; export const getVariables: RequestType = { method: 'getVariables' }; + +export const showAdvancedDisplayConfigurationType: NotificationType = { method: 'showAdvancedOptions' }; +export const getWebviewSelectionType: RequestType = { method: 'getWebviewSelection' }; + +export interface WebviewSelection { + selectedCell?: { + column: string + value: string + } + textSelection?: string; +} diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index faacb57..0f4363f 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -31,6 +31,9 @@ import { setMemoryViewSettingsType, resetMemoryViewSettingsType, setTitleType, + showAdvancedDisplayConfigurationType as showAdvancedOptionsConfigurationType, + getWebviewSelectionType, + WebviewSelection, } from '../common/messaging'; import { MemoryProvider } from './memory-provider'; import { outputChannelLogger } from './logger'; @@ -49,6 +52,12 @@ enum RefreshEnum { on = 1 } +export interface WebviewMenuContext { + messageParticipant: WebviewIdMessageParticipant, + visibleColumns: string, + showRadixPrefix: boolean, +} + const isMemoryVariable = (variable: Variable): variable is Variable => variable && !!(variable as Variable).memoryReference; const CONFIGURABLE_COLUMNS = [ manifest.CONFIG_SHOW_ASCII_COLUMN, @@ -59,6 +68,14 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { public static ViewType = `${manifest.PACKAGE_NAME}.memory`; public static ShowCommandType = `${manifest.PACKAGE_NAME}.show`; public static VariableCommandType = `${manifest.PACKAGE_NAME}.show-variable`; + public static ShowAsciiColumnCommandType = `${manifest.PACKAGE_NAME}.show-ascii-column`; + public static HideAsciiColumnCommandType = `${manifest.PACKAGE_NAME}.hide-ascii-column`; + public static ShowVariablesColumnCommandType = `${manifest.PACKAGE_NAME}.show-variables-column`; + public static HideVariablesColumnCommandType = `${manifest.PACKAGE_NAME}.hide-variables-column`; + public static ShowRadixPrefixCommandType = `${manifest.PACKAGE_NAME}.show-radix-prefix`; + public static HideRadixPrefixCommandType = `${manifest.PACKAGE_NAME}.hide-radix-prefix`; + public static ShowAdvancedDisplayConfigurationCommandType = `${manifest.PACKAGE_NAME}.show-advanced-display-options`; + public static GetWebviewSelectionCommandType = `${manifest.PACKAGE_NAME}.get-webview-selection`; protected messenger: Messenger; protected refreshOnStop: RefreshEnum; @@ -77,6 +94,10 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { } public activate(context: vscode.ExtensionContext): void { + const getVisibleColumns = + (columnContext: string): string[] => + columnContext.substring(1, columnContext.length - 1).split('|'); + context.subscriptions.push( vscode.window.registerCustomEditorProvider(manifest.EDITOR_NAME, this), vscode.commands.registerCommand(MemoryWebview.ShowCommandType, () => this.show()), @@ -85,7 +106,29 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { if (isMemoryVariable(variable)) { this.show({ memoryReference: variable.memoryReference.toString() }); } - }) + }), + vscode.commands.registerCommand(MemoryWebview.ShowVariablesColumnCommandType, (ctx: WebviewMenuContext) => { + this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns: [...getVisibleColumns(ctx.visibleColumns), 'variables'] }); + }), + vscode.commands.registerCommand(MemoryWebview.HideVariablesColumnCommandType, (ctx: WebviewMenuContext) => { + this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns: getVisibleColumns(ctx.visibleColumns).filter(column => column !== 'variables') }); + }), + vscode.commands.registerCommand(MemoryWebview.ShowAsciiColumnCommandType, (ctx: WebviewMenuContext) => { + this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns: [...getVisibleColumns(ctx.visibleColumns), 'ascii'] }); + }), + vscode.commands.registerCommand(MemoryWebview.HideAsciiColumnCommandType, (ctx: WebviewMenuContext) => { + this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns: getVisibleColumns(ctx.visibleColumns).filter(column => column !== 'ascii') }); + }), + vscode.commands.registerCommand(MemoryWebview.ShowRadixPrefixCommandType, (ctx: WebviewMenuContext) => { + this.setMemoryViewSettings(ctx.messageParticipant, { showRadixPrefix: true, visibleColumns: getVisibleColumns(ctx.visibleColumns) }); + }), + vscode.commands.registerCommand(MemoryWebview.HideRadixPrefixCommandType, (ctx: WebviewMenuContext) => { + this.setMemoryViewSettings(ctx.messageParticipant, { showRadixPrefix: false, visibleColumns: getVisibleColumns(ctx.visibleColumns) }); + }), + vscode.commands.registerCommand(MemoryWebview.ShowAdvancedDisplayConfigurationCommandType, async (ctx: WebviewMenuContext) => { + this.messenger.sendNotification(showAdvancedOptionsConfigurationType, ctx.messageParticipant, undefined); + }), + vscode.commands.registerCommand(MemoryWebview.GetWebviewSelectionCommandType, (ctx: WebviewMenuContext) => this.getWebviewSelection(ctx.messageParticipant)) ); }; @@ -213,10 +256,14 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { } protected setInitialSettings(webviewParticipant: WebviewIdMessageParticipant, title: string): void { - this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, this.getMemoryViewSettings(title)); + this.setMemoryViewSettings(webviewParticipant, this.getMemoryViewSettings(webviewParticipant, title)); } - protected getMemoryViewSettings(title: string): MemoryViewSettings { + protected setMemoryViewSettings(webviewParticipant: WebviewIdMessageParticipant, settings: Partial): void { + this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, settings); + } + + protected getMemoryViewSettings(messageParticipant: WebviewIdMessageParticipant, title: string): MemoryViewSettings { const memoryInspectorConfiguration = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); const bytesPerWord = memoryInspectorConfiguration.get(manifest.CONFIG_BYTES_PER_WORD, manifest.DEFAULT_BYTES_PER_WORD); const wordsPerGroup = memoryInspectorConfiguration.get(manifest.CONFIG_WORDS_PER_GROUP, manifest.DEFAULT_WORDS_PER_GROUP); @@ -227,7 +274,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { .map(columnId => columnId.replace('columns.', '')); 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); - return { title, bytesPerWord, wordsPerGroup, groupsPerRow, scrollingBehavior, visibleColumns, addressRadix, showRadixPrefix }; + return { messageParticipant, title, bytesPerWord, wordsPerGroup, groupsPerRow, scrollingBehavior, visibleColumns, addressRadix, showRadixPrefix }; } protected async readMemory(request: DebugProtocol.ReadMemoryArguments): Promise { @@ -254,4 +301,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { return []; } } + + protected getWebviewSelection(webviewParticipant: WebviewIdMessageParticipant): Promise { + return this.messenger.sendRequest(getWebviewSelectionType, webviewParticipant, undefined); + } } diff --git a/src/webview/columns/address-column.tsx b/src/webview/columns/address-column.tsx index 532c986..040c2fb 100644 --- a/src/webview/columns/address-column.tsx +++ b/src/webview/columns/address-column.tsx @@ -16,8 +16,9 @@ import React, { ReactNode } from 'react'; import { BigIntMemoryRange, getAddressString, getRadixMarker } from '../../common/memory-range'; -import { ColumnContribution } from './column-contribution-service'; +import { createVscodeContext } from '../utils/vscode-contexts'; import { Memory, MemoryDisplayConfiguration } from '../utils/view-types'; +import { ColumnContribution } from './column-contribution-service'; export class AddressColumn implements ColumnContribution { static ID = 'address'; @@ -27,7 +28,12 @@ export class AddressColumn implements ColumnContribution { readonly priority = 0; render(range: BigIntMemoryRange, _: Memory, options: MemoryDisplayConfiguration): ReactNode { - return + const cellContext = { + address: getAddressString(range.startAddress, options.addressRadix), + radix: getRadixMarker(options.addressRadix), + + }; + return {options.showRadixPrefix && {getRadixMarker(options.addressRadix)}} {getAddressString(range.startAddress, options.addressRadix)} ; diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index a9875da..c3d1beb 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -25,6 +25,8 @@ import { Decoration, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTr import isDeepEqual from 'fast-deep-equal'; import { AddressColumn } from '../columns/address-column'; import { classNames } from 'primereact/utils'; +import { createColumnVscodeContext, createSectionVscodeContext } from '../utils/vscode-contexts'; +import { WebviewSelection } from '../../common/messaging'; export interface MoreMemorySelectProps { count: number; @@ -117,8 +119,11 @@ interface MemoryRowData { endAddress: bigint; } +export interface MemoryTableCellSelection extends DataTableCellSelection { + textContent: string; +} interface MemoryTableState { - selection: DataTableCellSelection | null; + selection: MemoryTableCellSelection | null; } type MemorySizeOptions = Pick; @@ -136,6 +141,7 @@ namespace MemorySizeOptions { export class MemoryTable extends React.PureComponent { protected datatableRef = React.createRef>(); + protected sectionMenuContext = createSectionVscodeContext('memoryTable'); protected get isShowMoreEnabled(): boolean { return !!this.props.memory?.bytes.length; @@ -181,14 +187,16 @@ export class MemoryTable extends React.PureComponent +
ref={this.datatableRef} + onContextMenuCapture={this.onContextMenu} + {...this.sectionMenuContext} {...props} > {this.props.columnOptions.map(({ contribution }) => { const fit = contribution.id === AddressColumn.ID; - + const pt = { root: createColumnVscodeContext(contribution.id) }; return row && contribution.render(row, this.props.memory!, this.props)}> {contribution.label} ; @@ -214,6 +223,8 @@ export class MemoryTable extends React.PureComponent) => { - this.setState(prev => ({ ...prev, selection: event.value })); + // eslint-disable-next-line no-null/no-null + const value = event.value ? event.value as MemoryTableCellSelection : null; + if (value) { + value.textContent = event.originalEvent.currentTarget?.textContent ?? ''; + } + + this.setState(prev => ({ ...prev, selection: value })); + }; + + protected onCopy = (_event: React.ClipboardEvent) => { + // if we don't have a text selection, we copy the textContent of the currently selected cell + if (!window.getSelection()?.toString() && this.state.selection) { + navigator.clipboard.writeText(this.state.selection.textContent); + } + }; + + protected onContextMenu = (event: React.MouseEvent) => { + /* Context menu events for cells are triggered on the root td. + * The rendered child content might provide additional vscode context data. + * Per default this child context will not be available in the merged vscode context + * So we need to handle this case manually and merge the child context into the parent context. + */ + if (!(event.target instanceof HTMLElement)) { + return; + } + const parent = event.target.closest('.p-selectable-cell'); + if (!parent || !(parent instanceof HTMLTableCellElement)) { + return; + } + let currentElement: Element | undefined = event.target; + const childContexts: object[] = []; + while (currentElement && currentElement !== parent) { + if (currentElement instanceof HTMLElement && currentElement.dataset?.vscodeContext) { + childContexts.push(JSON.parse(currentElement.dataset['vscodeContext'])); + } + currentElement = currentElement.parentElement ?? undefined; + } + + if (childContexts.length > 0) { + const parentContext = JSON.parse(parent.dataset['vscodeContext'] ?? '{}'); + const mergedContext = childContexts.reduce((acc, cur) => ({ ...acc, ...cur }), parentContext); + parent.dataset['vscodeContext'] = JSON.stringify(mergedContext); + } }; protected renderHeader(): React.ReactNode | undefined { @@ -316,9 +369,14 @@ export class MemoryTable extends React.PureComponent { + protected optionsWidget = React.createRef(); + protected memoryTable = React.createRef(); constructor(props: MemoryWidgetProps) { super(props); this.state = { ...defaultOptions }; } + protected createVscodeContext(): VscodeContext { + const visibleColumns = `|${this.props.columns.filter(candidate => candidate.active).map(column => column.contribution.id).join('|')}|`; + return createAppVscodeContext({ + messageParticipant: this.props.messageParticipant, + showRadixPrefix: this.props.showRadixPrefix, + visibleColumns + }); + + } + override render(): React.ReactNode { - return (
+ return (
candidate.active)} memory={this.props.memory} @@ -95,7 +113,15 @@ export class MemoryWidget extends React.Component -
); +
); + } + + public showAdvancedOptions(): void { + this.optionsWidget.current?.showAdvancedOptions(); + } + + public getWebviewSelection(): WebviewSelection { + return this.memoryTable.current?.getWebviewSelection() ?? {}; } } diff --git a/src/webview/components/options-widget.tsx b/src/webview/components/options-widget.tsx index b6ef6d3..ef1e742 100644 --- a/src/webview/components/options-widget.tsx +++ b/src/webview/components/options-widget.tsx @@ -28,6 +28,7 @@ import { } from '../utils/view-types'; import { MultiSelectWithLabel } from './multi-select'; import { Checkbox } from 'primereact/checkbox'; +import { createSectionVscodeContext } from '../utils/vscode-contexts'; export interface OptionsWidgetProps extends Omit, @@ -74,6 +75,9 @@ export class OptionsWidget extends React.Component; protected extendedOptions = React.createRef(); protected labelEditInput = React.createRef(); + protected coreOptionsDiv = React.createRef(); + protected optionsMenuContext = createSectionVscodeContext('optionsWidget'); + protected advancedOptionsContext = createSectionVscodeContext('advancedOptionsOverlay'); protected get optionsFormValues(): OptionsForm { return { @@ -146,7 +150,7 @@ export class OptionsWidget extends React.Component +
-
- +
+ {formik => (
@@ -257,7 +261,7 @@ export class OptionsWidget extends React.Component - +