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
- 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 8, 2024
1 parent 51e738c commit 43b5073
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 19 deletions.
46 changes: 46 additions & 0 deletions package.json
Expand Up @@ -81,6 +81,30 @@
"command": "memory-inspector.show-variable",
"title": "Show in Memory Inspector",
"category": "Memory"
},
{
"command": "memory-inspector.toggle-variables-column",
"title": "Toggle Variables Column",
"category": "Memory",
"enablement": "webviewId === memory-inspector.memory"
},
{
"command": "memory-inspector.toggle-ascii-column",
"title": "Toggle ASCII Column",
"category": "Memory",
"enablement": "webviewId === memory-inspector.memory"
},
{
"command": "memory-inspector.toggle-radix-prefix",
"title": "Toggle 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 +129,28 @@
"command": "memory-inspector.show-variable",
"when": "canViewMemory && memory-inspector.canRead"
}
],
"webview/context": [
{
"command": "memory-inspector.toggle-variables-column",
"group": "display@1",
"when": "webviewId === memory-inspector.memory"
},
{
"command": "memory-inspector.toggle-ascii-column",
"group": "display@2",
"when": "webviewId === memory-inspector.memory"
},
{
"command": "memory-inspector.toggle-radix-prefix",
"group": "display@3",
"when": "webviewId === memory-inspector.memory"
},
{
"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 showAdvancedOptionsType: NotificationType<void> = { method: 'showAdvancedOptions' };
export const getWebviewSelectionType: RequestType<void, WebviewSelection> = { method: 'getWebviewSelection' };

export interface WebviewSelection {
selectedCell?: {
column: string
value: string
}
textSelection?: string;
}
50 changes: 50 additions & 0 deletions src/common/webview-context.ts
@@ -0,0 +1,50 @@
/********************************************************************************
* Copyright (C) 2024 EclipseSource 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 { WebviewIdMessageParticipant } from 'vscode-messenger-common';
import { VariableMetadata } from './memory-range';

export interface WebviewContext {
messageParticipant: WebviewIdMessageParticipant,
webviewSection: string,
showAsciiColumn: boolean
showVariablesColumn: boolean,
showRadixPrefix: boolean,
}

export interface WebviewCellContext extends WebviewContext {
column: string;
value: string;
}

export interface WebviewVariableContext extends WebviewCellContext {
variable?: VariableMetadata
}

/**
* Retrieves the currently visible (configurable) columns from the given {@link WebviewContext}.
* @returns A string array containing the visible columns ids.
*/
export function getVisibleColumns(context: WebviewContext): string[] {
const columns = [];
if (context.showAsciiColumn) {
columns.push('ascii');
}
if (context.showVariablesColumn) {
columns.push('variables');
}
return columns;
}
50 changes: 46 additions & 4 deletions src/plugin/memory-webview-main.ts
Expand Up @@ -31,11 +31,15 @@ import {
setMemoryViewSettingsType,
resetMemoryViewSettingsType,
setTitleType,
showAdvancedOptionsType,
getWebviewSelectionType,
WebviewSelection,
} from '../common/messaging';
import { MemoryProvider } from './memory-provider';
import { outputChannelLogger } from './logger';
import { Endianness, VariableRange } from '../common/memory-range';
import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types';
import { WebviewContext, getVisibleColumns } from '../common/webview-context';

interface Variable {
name: string;
Expand All @@ -59,6 +63,11 @@ 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 ToggleAsciiColumnCommandType = `${manifest.PACKAGE_NAME}.toggle-ascii-column`;
public static ToggleVariablesColumnCommandType = `${manifest.PACKAGE_NAME}.toggle-variables-column`;
public static ToggleRadixPrefixCommandType = `${manifest.PACKAGE_NAME}.toggle-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 @@ -85,7 +94,20 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider {
if (isMemoryVariable(variable)) {
this.show({ memoryReference: variable.memoryReference.toString() });
}
})
}),
vscode.commands.registerCommand(MemoryWebview.ToggleVariablesColumnCommandType, (ctx: WebviewContext) => {
this.toggleWebviewColumn(ctx, 'variables');
}),
vscode.commands.registerCommand(MemoryWebview.ToggleAsciiColumnCommandType, (ctx: WebviewContext) => {
this.toggleWebviewColumn(ctx, 'ascii');
}),
vscode.commands.registerCommand(MemoryWebview.ToggleRadixPrefixCommandType, (ctx: WebviewContext) => {
this.setMemoryViewSettings(ctx.messageParticipant, { showRadixPrefix: !ctx.showRadixPrefix });
}),
vscode.commands.registerCommand(MemoryWebview.ShowAdvancedDisplayConfigurationCommandType, async (ctx: WebviewContext) => {
this.messenger.sendNotification(showAdvancedOptionsType, ctx.messageParticipant, undefined);
}),
vscode.commands.registerCommand(MemoryWebview.GetWebviewSelectionCommandType, (ctx: WebviewContext) => this.getWebviewSelection(ctx.messageParticipant)),
);
};

Expand Down Expand Up @@ -213,10 +235,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 setMemoryViewSettings(webviewParticipant: WebviewIdMessageParticipant, settings: Partial<MemoryViewSettings>): void {
this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, settings);
}

protected getMemoryViewSettings(title: string): MemoryViewSettings {
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 @@ -229,7 +255,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, endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix };
return { messageParticipant, title, bytesPerWord, wordsPerGroup, groupsPerRow, endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix };
}

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

protected getWebviewSelection(webviewParticipant: WebviewIdMessageParticipant): Promise<WebviewSelection> {
return this.messenger.sendRequest(getWebviewSelectionType, webviewParticipant, undefined);
}

protected toggleWebviewColumn(ctx: WebviewContext, column: string): void {
const visibleColumns = getVisibleColumns(ctx);
const index = visibleColumns.indexOf(column);
if (index === -1) {
visibleColumns.push(column);
} else {
visibleColumns.splice(index, 1);
}

this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns });
}
}
2 changes: 1 addition & 1 deletion src/webview/columns/data-column.tsx
Expand Up @@ -35,7 +35,7 @@ export class DataColumn implements ColumnContribution {
};

render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode {
return <div className='data-groups-container'>{this.renderGroups(range, memory, options)}</div>;
return <span className='data-groups-container'>{this.renderGroups(range, memory, options)}</span>;
}

protected renderGroups(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode {
Expand Down
76 changes: 73 additions & 3 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 {
activeReadArguments: Required<DebugProtocol.ReadMemoryArguments>;
Expand Down Expand Up @@ -144,13 +146,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 @@ -169,6 +174,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab

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

protected get datatableWrapper(): HTMLElement | undefined {
return this.datatableRef.current?.getElement().querySelector<HTMLElement>('[data-pc-section="wrapper"]') ?? undefined;
Expand Down Expand Up @@ -265,16 +271,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 @@ -283,6 +292,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 @@ -301,6 +311,9 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
metaKeySelection: false,
onSelectionChange: this.onSelectionChanged,
onColumnResizeEnd: this.onColumnResizeEnd,
onContextMenuCapture: this.onContextMenu,
onCopy: this.onCopy,
onCut: this.onCut,
resizableColumns: true,
scrollable: true,
scrollHeight: 'flex',
Expand Down Expand Up @@ -372,13 +385,65 @@ 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) => {
event.preventDefault();
const textSelection = window.getSelection()?.toString();
if (!textSelection) {
if (this.state.selection) {
navigator.clipboard.writeText(this.state.selection.textContent);
}
return;
}

if (this.props.groupsPerRow !== 'Autofit') {
navigator.clipboard.writeText(textSelection);
return;
}

// If the autofit is enabled, we need to temporarily remove the class to calculate the correct selection
if (event.currentTarget instanceof HTMLDivElement) {
event.currentTarget.classList.remove('groups-per-row-autofit');
const correctedSelection = window.getSelection()!.toString();
event.currentTarget.classList.add('groups-per-row-autofit');
navigator.clipboard.writeText(correctedSelection);
}
};

protected onCut = (event: React.ClipboardEvent) => this.onCopy(event);

protected onContextMenu = (event: React.MouseEvent) => {
if (!(event.target instanceof HTMLElement)) {
return;
}

const cell = event.target.closest('.p-selectable-cell');
if (!cell || !(cell instanceof HTMLTableCellElement)) {
return;
}

/*
* Before opening a context menu for a table cell target we dynamically add the `value` property to the <vscode-data-context.
* Using this dynamic approach ensures the the cell value is also set correctly when the menu was opened on empty cell space.
*/
const value = cell.textContent;
const cellContext = JSON.parse(cell.dataset.vscodeContext ?? '{}');
cellContext.value = value;
cell.dataset.vscodeContext = JSON.stringify(cellContext);
};

protected renderHeader(): React.ReactNode | undefined {
let memorySelect: React.ReactNode | undefined;
let loading: React.ReactNode | undefined;
Expand Down Expand Up @@ -496,6 +561,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 43b5073

Please sign in to comment.