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 6, 2024
1 parent 7416dc9 commit cf40382
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 17 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 @@ -228,7 +275,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider {
const addressPadding = AddressPaddingOptions[memoryInspectorConfiguration.get(manifest.CONFIG_ADDRESS_PADDING, manifest.DEFAULT_ADDRESS_PADDING)];
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, addressPadding, addressRadix, showRadixPrefix };
return { messageParticipant, title, bytesPerWord, wordsPerGroup, groupsPerRow, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix };
}

protected async readMemory(request: DebugProtocol.ReadMemoryArguments): Promise<MemoryReadResult> {
Expand All @@ -255,4 +302,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider {
return [];
}
}

protected getWebviewSelection(webviewParticipant: WebviewIdMessageParticipant): Promise<WebviewSelection> {
return this.messenger.sendRequest(getWebviewSelectionType, webviewParticipant, undefined);
}
}
67 changes: 63 additions & 4 deletions src/webview/components/memory-table.tsx
Expand Up @@ -26,6 +26,8 @@ import isDeepEqual from 'fast-deep-equal';
import { classNames } from 'primereact/utils';
import { tryToNumber } from '../../common/typescript';
import { DataColumn } from '../columns/data-column';
import { createColumnVscodeContext, createSectionVscodeContext } from '../utils/vscode-contexts';
import { WebviewSelection } from '../../common/messaging';

export interface MoreMemorySelectProps {
count: number;
Expand Down Expand Up @@ -119,13 +121,16 @@ interface MemoryRowData {
endAddress: bigint;
}

export interface MemoryTableCellSelection extends DataTableCellSelection<MemoryRowData[]> {
textContent: string;
}
interface MemoryTableState {
/**
* The value coming from {@link MemoryTableProps.groupsPerRow} can have non-numeric values such as `Autofit`.
* For this reason, we need to transform the provided value to a numeric one to render correctly.
*/
groupsPerRowToRender: number;
selection: DataTableCellSelection<MemoryRowData[]> | null;
selection: MemoryTableCellSelection | null;
}

export type MemorySizeOptions = Pick<MemoryTableProps, 'bytesPerWord' | 'wordsPerGroup'> & { groupsPerRow: number };
Expand All @@ -144,6 +149,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab

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

protected get isShowMoreEnabled(): boolean {
return !!this.props.memory?.bytes.length;
Expand Down Expand Up @@ -211,16 +217,19 @@ 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 isContentWidthFit = contribution.fittingType === 'content-width';
const className = classNames(contribution.className, {
'content-width-fit': isContentWidthFit
});
const pt = { root: createColumnVscodeContext(contribution.id) };

return <Column
key={contribution.id}
Expand All @@ -229,6 +238,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
className={className}
headerClassName={className}
style={{ width: isContentWidthFit ? undefined : `${columnWidth}%` }}
pt={pt}
body={(row?: MemoryRowData) => row && contribution.render(row, this.props.memory!, this.props)}>
{contribution.label}
</Column>;
Expand All @@ -248,6 +258,8 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
metaKeySelection: false,
onSelectionChange: this.onSelectionChanged,
onColumnResizeEnd: this.onColumnResizeEnd,
onContextMenuCapture: this.onContextMenu,
onCopy: this.onCopy,
resizableColumns: true,
scrollable: true,
scrollHeight: 'flex',
Expand All @@ -259,13 +271,55 @@ 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 onColumnResizeEnd = () => {
this.autofitColumns();
};

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 {
const { offset, count, fetchMemory, scrollingBehavior } = this.props;

Expand Down Expand Up @@ -353,7 +407,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
return {
rowIndex,
startAddress,
endAddress: startAddress + memoryTableOptions.bigWordsPerRow
endAddress: startAddress + memoryTableOptions.bigWordsPerRow,
};
}

Expand Down Expand Up @@ -388,6 +442,11 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab

return options.groupsPerRow;
}

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 cf40382

Please sign in to comment.