Skip to content

Commit

Permalink
Context menu enhancements
Browse files Browse the repository at this point in the history
Enhances the memory webview to  properly support context menu actions contributed via `webview/context` contribution point. This includes
- Augmenting the webview components with `data-vscode-context` custom data properties.
 These properties are used to provide additional context info (as JSON string). The composed context is then available in `when` conditions from `webview/context` contributions and as command argument when executing the associated command.

Provides the following context menu enhancements outlined in eclipse-cdt-cloud#51
- Copy selection
- Quick access to window configuration by
  - providing show/hide entries for variable & ascii column
  - show/hide for radix prefix
  - `Show advanced Options` command to show the advanced settings overlay
 - Allow contributions from other extensions
  • Loading branch information
tortmayr committed Mar 5, 2024
1 parent 531bea1 commit d06ecec
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 20 deletions.
83 changes: 83 additions & 0 deletions package.json
Expand Up @@ -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": {
Expand All @@ -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": [
Expand Down
11 changes: 11 additions & 0 deletions src/common/messaging.ts
Expand Up @@ -31,3 +31,14 @@ export const setOptionsType: RequestType<Partial<DebugProtocol.ReadMemoryArgumen
export const readMemoryType: RequestType<DebugProtocol.ReadMemoryArguments, MemoryReadResult> = { method: 'readMemory' };
export const writeMemoryType: RequestType<DebugProtocol.WriteMemoryArguments, MemoryWriteResult> = { method: 'writeMemory' };
export const getVariables: RequestType<DebugProtocol.ReadMemoryArguments, VariableRange[]> = { method: 'getVariables' };

export const showAdvancedDisplayConfigurationType: NotificationType<void> = { method: 'showAdvancedOptions' };
export const getWebviewSelectionType: RequestType<void, WebviewSelection> = { method: 'getWebviewSelection' };

export interface WebviewSelection {
selectedCell?: {
column: string
value: string
}
textSelection?: string;
}
59 changes: 55 additions & 4 deletions src/plugin/memory-webview-main.ts
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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()),
Expand All @@ -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))
);
};

Expand Down Expand Up @@ -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<MemoryViewSettings>): 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<number>(manifest.CONFIG_BYTES_PER_WORD, manifest.DEFAULT_BYTES_PER_WORD);
const wordsPerGroup = memoryInspectorConfiguration.get<number>(manifest.CONFIG_WORDS_PER_GROUP, manifest.DEFAULT_WORDS_PER_GROUP);
Expand All @@ -227,7 +274,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider {
.map(columnId => columnId.replace('columns.', ''));
const addressRadix = memoryInspectorConfiguration.get<number>(manifest.CONFIG_ADDRESS_RADIX, manifest.DEFAULT_ADDRESS_RADIX);
const showRadixPrefix = memoryInspectorConfiguration.get<boolean>(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<MemoryReadResult> {
Expand All @@ -254,4 +301,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider {
return [];
}
}

protected getWebviewSelection(webviewParticipant: WebviewIdMessageParticipant): Promise<WebviewSelection> {
return this.messenger.sendRequest(getWebviewSelectionType, webviewParticipant, undefined);
}
}
10 changes: 8 additions & 2 deletions src/webview/columns/address-column.tsx
Expand Up @@ -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';
Expand All @@ -27,7 +28,12 @@ export class AddressColumn implements ColumnContribution {
readonly priority = 0;

render(range: BigIntMemoryRange, _: Memory, options: MemoryDisplayConfiguration): ReactNode {
return <span className='memory-start-address'>
const cellContext = {
address: getAddressString(range.startAddress, options.addressRadix),
radix: getRadixMarker(options.addressRadix),

};
return <span className='memory-start-address' {...createVscodeContext(cellContext)}>
{options.showRadixPrefix && <span className='radix-prefix'>{getRadixMarker(options.addressRadix)}</span>}
<span className='address'>{getAddressString(range.startAddress, options.addressRadix)}</span>
</span>;
Expand Down
68 changes: 63 additions & 5 deletions src/webview/components/memory-table.tsx
Expand Up @@ -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;
Expand Down Expand Up @@ -117,8 +119,11 @@ interface MemoryRowData {
endAddress: bigint;
}

export interface MemoryTableCellSelection extends DataTableCellSelection<MemoryRowData[]> {
textContent: string;
}
interface MemoryTableState {
selection: DataTableCellSelection<MemoryRowData[]> | null;
selection: MemoryTableCellSelection | null;
}

type MemorySizeOptions = Pick<MemoryTableProps, 'bytesPerWord' | 'wordsPerGroup' | 'groupsPerRow'>;
Expand All @@ -136,6 +141,7 @@ namespace MemorySizeOptions {
export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTableState> {

protected datatableRef = React.createRef<DataTable<MemoryRowData[]>>();
protected sectionMenuContext = createSectionVscodeContext('memoryTable');

protected get isShowMoreEnabled(): boolean {
return !!this.props.memory?.bytes.length;
Expand Down Expand Up @@ -181,21 +187,24 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
const columnWidth = remainingWidth / (this.props.columnOptions.length);

return (
<div className='flex-1 overflow-auto px-4'>
<div className='flex-1 overflow-auto px-4' >
<DataTable<MemoryRowData[]>
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 <Column
key={contribution.id}
field={contribution.id}
header={contribution.label}
className={classNames({ fit })}
headerClassName={classNames({ fit })}
style={{ width: fit ? undefined : `${columnWidth}%` }}
pt={pt}
body={(row?: MemoryRowData) => row && contribution.render(row, this.props.memory!, this.props)}>
{contribution.label}
</Column>;
Expand All @@ -214,6 +223,8 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
lazy: true,
metaKeySelection: false,
onSelectionChange: this.onSelectionChanged,
onContextMenuCapture: this.onContextMenu,
onCopy: this.onCopy,
resizableColumns: true,
scrollable: true,
scrollHeight: 'flex',
Expand All @@ -226,7 +237,49 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
}

protected onSelectionChanged = (event: DataTableSelectionCellChangeEvent<MemoryRowData[]>) => {
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 {
Expand Down Expand Up @@ -316,9 +369,14 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
return {
rowIndex,
startAddress,
endAddress: startAddress + memoryTableOptions.bigWordsPerRow
endAddress: startAddress + memoryTableOptions.bigWordsPerRow,
};
}

public getWebviewSelection(): WebviewSelection {
const textSelection = window.getSelection()?.toString() ?? '';
return this.state.selection ? { textSelection, selectedCell: { column: this.state.selection.field, value: this.state.selection.textContent } } : { textSelection };
}
}

export namespace MemoryTable {
Expand Down

0 comments on commit d06ecec

Please sign in to comment.