Skip to content

Commit

Permalink
Context menu enhancements (#94)
Browse files Browse the repository at this point in the history
* Context menu enhancements

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 #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 11, 2024
1 parent 51e738c commit 68ad5ca
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 18 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;
}
53 changes: 49 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,10 @@ 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 +285,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 });
}
}
63 changes: 60 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,52 @@ 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) {
navigator.clipboard.writeText(textSelection);
} else if (this.state.selection) {
navigator.clipboard.writeText(this.state.selection.textContent);

}
};

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 +548,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 68ad5ca

Please sign in to comment.