diff --git a/media/memory-table.css b/media/memory-table.css
index 30e2911..5497614 100644
--- a/media/memory-table.css
+++ b/media/memory-table.css
@@ -81,3 +81,59 @@
.radix-prefix {
opacity: 0.6;
}
+
+.hoverable {
+ position: relative;
+}
+
+/* Basic hover formatting (copied from Monaco hovers) */
+.memory-hover {
+ min-width: fit-content;
+ max-width: var(--vscode-hover-maxWidth,500px);
+ border: 1px solid var(--vscode-editorHoverWidget-border);
+ border-radius: 3px;
+
+ color: var(--vscode-editorHoverWidget-foreground);
+ background-color: var(--vscode-editorHoverWidget-background);
+
+ font-family: var(--vscode-editor-font-family);
+ font-size: var(--vscode-font-size);
+}
+
+/* Table formatting for hovers */
+.memory-hover table {
+ border-collapse: collapse;
+ border-style: hidden;
+}
+.memory-hover table caption {
+ padding: 4px;
+ border-bottom: 1px solid var(--vscode-editorHoverWidget-border);
+}
+.memory-hover td {
+ border: 1px solid var(--vscode-editorHoverWidget-border);
+ padding: 2px 8px;
+}
+.memory-hover td:first-child {
+ text-align: right;
+}
+
+/* Colors for the hover fields */
+.memory-hover .label-value-pair>.label {
+ color: var(--vscode-debugTokenExpression-string);
+}
+.memory-hover .label-value-pair>.value {
+ color: var(--vscode-debugTokenExpression-number);
+}
+
+/* Colors for specific hover fields */
+.memory-hover .address-hover .primary {
+ background-color: var(--vscode-list-hoverBackground);
+}
+.memory-hover table caption {
+ color: var(--vscode-symbolIcon-variableForeground);
+}
+.memory-hover .address-hover .value.utf8,
+.memory-hover .data-hover .value.utf8,
+.memory-hover .variable-hover .value.type {
+ color: var(--vscode-debugTokenExpression-name);
+}
diff --git a/src/plugin/adapter-registry/c-tracker.ts b/src/plugin/adapter-registry/c-tracker.ts
index ef211f0..2d709e0 100644
--- a/src/plugin/adapter-registry/c-tracker.ts
+++ b/src/plugin/adapter-registry/c-tracker.ts
@@ -45,6 +45,7 @@ export class CTracker extends AdapterVariableTracker {
startAddress: toHexStringWithRadixMarker(startAddress),
endAddress: endAddress === undefined ? undefined : toHexStringWithRadixMarker(endAddress),
value: variable.value,
+ type: variable.type,
};
} catch (err) {
this.logger.warn('Unable to resolve location and size of', variable.name + (err instanceof Error ? ':\n\t' + err.message : ''));
diff --git a/src/webview/columns/address-column.tsx b/src/webview/columns/address-column.tsx
index 532c986..a1c809f 100644
--- a/src/webview/columns/address-column.tsx
+++ b/src/webview/columns/address-column.tsx
@@ -29,7 +29,7 @@ export class AddressColumn implements ColumnContribution {
render(range: BigIntMemoryRange, _: Memory, options: MemoryDisplayConfiguration): ReactNode {
return
{options.showRadixPrefix && {getRadixMarker(options.addressRadix)}}
- {getAddressString(range.startAddress, options.addressRadix)}
+ {getAddressString(range.startAddress, options.addressRadix)}
;
}
}
diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx
index 8510222..791d9b3 100644
--- a/src/webview/columns/data-column.tsx
+++ b/src/webview/columns/data-column.tsx
@@ -35,11 +35,11 @@ export class DataColumn implements ColumnContribution {
for (let i = range.startAddress; i < range.endAddress; i++) {
words.push(this.renderWord(memory, options, i));
if (words.length % options.wordsPerGroup === 0) {
- groups.push({words});
+ groups.push({words});
words = [];
}
}
- if (words.length) { groups.push({words}); }
+ if (words.length) { groups.push({words}); }
return groups;
}
diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx
index a9875da..0fefae9 100644
--- a/src/webview/components/memory-table.tsx
+++ b/src/webview/components/memory-table.tsx
@@ -19,12 +19,15 @@ import memoize from 'memoize-one';
import { Column } from 'primereact/column';
import { DataTable, DataTableCellSelection, DataTableProps, DataTableSelectionCellChangeEvent } from 'primereact/datatable';
import { ProgressSpinner } from 'primereact/progressspinner';
+import { Tooltip } from 'primereact/tooltip';
import React from 'react';
import { TableRenderOptions } from '../columns/column-contribution-service';
import { Decoration, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types';
import isDeepEqual from 'fast-deep-equal';
import { AddressColumn } from '../columns/address-column';
import { classNames } from 'primereact/utils';
+import type { HoverService } from '../hovers/hover-service';
+import { TooltipEvent } from 'primereact/tooltip/tooltipoptions';
export interface MoreMemorySelectProps {
count: number;
@@ -98,6 +101,7 @@ export const MoreMemorySelect: React.FC = ({ count, offse
interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguration {
memory?: Memory;
decorations: Decoration[];
+ hovers: HoverService;
offset: number;
count: number;
fetchMemory(partialOptions?: Partial): Promise;
@@ -119,6 +123,7 @@ interface MemoryRowData {
interface MemoryTableState {
selection: DataTableCellSelection | null;
+ hoverContent: React.ReactNode;
}
type MemorySizeOptions = Pick;
@@ -151,6 +156,7 @@ export class MemoryTable extends React.PureComponent>,
};
}
@@ -182,6 +188,11 @@ export class MemoryTable extends React.PureComponent
+ {this.state.hoverContent}
ref={this.datatableRef}
{...props}
@@ -319,6 +330,27 @@ export class MemoryTable extends React.PureComponent => {
+ const textContent = event.target.textContent ?? '';
+ const columnId = event.target.dataset.column ?? '';
+ let extraData = {};
+ try {
+ extraData = JSON.parse(event.target.dataset[columnId] ?? '{}');
+ } catch { /* no-op */ }
+ const node = await this.props.hovers.render({ columnId, textContent, extraData });
+ this.setState(prev => ({
+ ...prev,
+ hoverContent: node,
+ }));
+ };
+
+ protected handleOnTooltipHide = (): void => {
+ this.setState(prev => ({
+ ...prev,
+ hoverContent: <>>,
+ }));
+ };
}
export namespace MemoryTable {
diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx
index 4d75877..dead649 100644
--- a/src/webview/components/memory-widget.tsx
+++ b/src/webview/components/memory-widget.tsx
@@ -20,11 +20,13 @@ import { ColumnStatus } from '../columns/column-contribution-service';
import { Decoration, Endianness, Memory, MemoryDisplayConfiguration } from '../utils/view-types';
import { MemoryTable } from './memory-table';
import { OptionsWidget } from './options-widget';
+import { HoverService } from '../hovers/hover-service';
interface MemoryWidgetProps extends MemoryDisplayConfiguration {
memory?: Memory;
title: string;
decorations: Decoration[];
+ hovers: HoverService;
columns: ColumnStatus[];
memoryReference: string;
offset: number;
@@ -80,6 +82,7 @@ export class MemoryWidget extends React.Component
candidate.active)}
memory={this.props.memory}
endianness={this.state.endianness}
diff --git a/src/webview/hovers/address-hover.tsx b/src/webview/hovers/address-hover.tsx
new file mode 100644
index 0000000..2e2a9df
--- /dev/null
+++ b/src/webview/hovers/address-hover.tsx
@@ -0,0 +1,89 @@
+/********************************************************************************
+ * Copyright (C) 2024 Ericsson 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 * as React from 'react';
+import { Radix } from '../../common/memory-range';
+import { HoverContribution, MemoryDetails } from './hover-service';
+
+export class AddressHover implements HoverContribution {
+ readonly id = 'address-hover';
+ priority = 0;
+ async render({ columnId, textContent, addressRadix }: MemoryDetails): Promise {
+ if (columnId !== 'address') { return; }
+
+ let binary = '';
+ let octal = '';
+ let decimal = '';
+ let hexadecimal = '';
+ let num = 0;
+ let primaryRadix = '';
+
+ switch (addressRadix) {
+ case Radix.Binary:
+ primaryRadix = 'binary';
+ binary = textContent;
+ num = parseInt(binary, 2);
+ octal = num.toString(8);
+ decimal = num.toString(10);
+ hexadecimal = num.toString(16);
+ break;
+ case Radix.Octal:
+ primaryRadix = 'octal';
+ octal = textContent;
+ num = parseInt(octal, 8);
+ binary = num.toString(2);
+ decimal = num.toString(10);
+ hexadecimal = num.toString(16);
+ break;
+ case Radix.Decimal:
+ primaryRadix = 'decimal';
+ decimal = textContent;
+ num = parseInt(decimal, 10);
+ binary = num.toString(2);
+ octal = num.toString(8);
+ hexadecimal = num.toString(16);
+ break;
+ case Radix.Hexadecimal:
+ primaryRadix = 'hexadecimal';
+ hexadecimal = textContent;
+ num = parseInt(hexadecimal, 16);
+ binary = num.toString(2);
+ octal = num.toString(8);
+ decimal = num.toString(10);
+ break;
+ default: return;
+ }
+
+ const hexCodePoint = (parseInt(hexadecimal.slice(-6), 16) > 0x10FFFF)
+ ? parseInt(hexadecimal.slice(-5), 16)
+ : parseInt(hexadecimal.slice(-6), 16);
+ const utf8 = String.fromCodePoint(hexCodePoint);
+
+ const hoverItem = (
+
+ {Object.entries({ binary, octal, decimal, hexadecimal, utf8 }).map(([label, value]) =>
+ value
+ ?
+ {label} |
+ {value} |
+
+ : ''
+ )}
+
+ );
+ return hoverItem;
+ }
+}
diff --git a/src/webview/hovers/data-hover.tsx b/src/webview/hovers/data-hover.tsx
new file mode 100644
index 0000000..f9ec622
--- /dev/null
+++ b/src/webview/hovers/data-hover.tsx
@@ -0,0 +1,51 @@
+/********************************************************************************
+ * Copyright (C) 2024 Ericsson 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 * as React from 'react';
+import { HoverContribution, MemoryDetails } from './hover-service';
+
+export class DataHover implements HoverContribution {
+ readonly id = 'data-hover';
+ priority = 0;
+ async render({ columnId, textContent }: MemoryDetails): Promise {
+ if (columnId !== 'data') { return; }
+
+ const hexadecimal = textContent;
+ const num = parseInt(hexadecimal, 16);
+ const binary = num.toString(2);
+ const octal = num.toString(8);
+ const decimal = num.toString(10);
+
+ const hexCodePoint = (parseInt(hexadecimal.slice(-6), 16) > 0x10FFFF)
+ ? parseInt(hexadecimal.slice(-5), 16)
+ : parseInt(hexadecimal.slice(-6), 16);
+ const utf8 = String.fromCodePoint(hexCodePoint);
+
+ const hoverItem = (
+
+ {Object.entries({ binary, octal, decimal, hexadecimal, utf8 }).map(([label, value]) =>
+ value
+ ?
+ {label} |
+ {value} |
+
+ : ''
+ )}
+
+ );
+ return hoverItem;
+ }
+}
diff --git a/src/webview/hovers/hover-service.tsx b/src/webview/hovers/hover-service.tsx
new file mode 100644
index 0000000..977320a
--- /dev/null
+++ b/src/webview/hovers/hover-service.tsx
@@ -0,0 +1,98 @@
+/********************************************************************************
+ * Copyright (C) 2024 Ericsson 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 * as React from 'react';
+import { HOST_EXTENSION } from 'vscode-messenger-common';
+import { logMessageType } from '../../common/messaging';
+import { Disposable, MemoryDisplayConfiguration } from '../utils/view-types';
+import { messenger } from '../view-messenger';
+import { MemoryAppState } from '../memory-webview-view';
+
+export interface HoverableDetails {
+ columnId: string;
+ textContent: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ extraData: any;
+}
+
+export interface MemoryDetails extends HoverableDetails, MemoryDisplayConfiguration, MemoryAppState { };
+
+export type HoverProvider = (data: MemoryDetails) => Promise;
+
+export interface HoverContribution {
+ readonly id: string;
+ priority?: number;
+ render: HoverProvider;
+}
+
+export class HoverService {
+ protected contributions: HoverContribution[] = [];
+
+ public register(contribution: HoverContribution): Disposable {
+ if (this.contributions.some(c => c.id === contribution.id)) { return { dispose: () => { } }; }
+ this.contributions.push(contribution);
+ this.contributions.sort(this.sortContributions);
+ return {
+ dispose: () => {
+ this.contributions = this.contributions.filter(hover => hover === contribution);
+ }
+ };
+ }
+
+ protected memoryAppState: MemoryAppState = {} as unknown as MemoryAppState;
+ public setMemoryState(state: MemoryAppState): void {
+ this.memoryAppState = state;
+ }
+
+ protected prepareData(hoverableDetails: HoverableDetails): MemoryDetails {
+ return {
+ ...hoverableDetails,
+ ...this.memoryAppState,
+ };
+ }
+
+ public async render(hoverableDetails: HoverableDetails): Promise {
+ const data = this.prepareData(hoverableDetails);
+ const promises = this.contributions.map(async contribution => {
+ let hoverPart: React.ReactNode;
+ try {
+ hoverPart = await contribution.render(data);
+ } catch (err) {
+ messenger.sendRequest(logMessageType, HOST_EXTENSION, `Error in hover contribution ${contribution.id}: ${err}`);
+ }
+ return hoverPart;
+ });
+ const nodes = (await Promise.all(promises)).filter(node => !!node);
+ if (nodes.length > 0) {
+ return {nodes}
;
+ }
+ return <>>;
+ }
+
+ protected sortContributions(left: HoverContribution, right: HoverContribution): number {
+ const leftHasPriority = typeof left.priority === 'number';
+ const rightHasPriority = typeof right.priority === 'number';
+ if (leftHasPriority && !rightHasPriority) { return -1; }
+ if (rightHasPriority && !leftHasPriority) { return 1; }
+ if ((!rightHasPriority && !leftHasPriority) || (left.priority! - right.priority! === 0)) {
+ return left.id.localeCompare(right.id);
+ }
+ return left.priority! - right.priority!;
+ }
+
+}
+
+export const hoverService = new HoverService();
diff --git a/src/webview/hovers/variable-hover.tsx b/src/webview/hovers/variable-hover.tsx
new file mode 100644
index 0000000..75333e0
--- /dev/null
+++ b/src/webview/hovers/variable-hover.tsx
@@ -0,0 +1,52 @@
+/********************************************************************************
+ * Copyright (C) 2024 Ericsson 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 * as React from 'react';
+import { VariableRange } from '../../common/memory-range';
+import { HoverContribution, MemoryDetails } from './hover-service';
+
+export class VariableHover implements HoverContribution {
+ readonly id = 'variable-hover';
+ priority = 0;
+
+ async render(
+ { columnId, bytesPerWord, extraData }: MemoryDetails,
+ ): Promise {
+ if (columnId !== 'variables') { return; }
+
+ const { type, startAddress, endAddress, name } = extraData as VariableRange;
+ const start = '0x' + parseInt(startAddress).toString(16);
+ const end = '0x' + parseInt(endAddress || '0').toString(16);
+ const words = (startAddress && endAddress) ? parseInt(endAddress) - parseInt(startAddress) : undefined;
+ const bytes = words ? words * bytesPerWord : undefined;
+
+ const hoverItem = (
+
+ {name}
+ {Object.entries({ type, start, end, words, bytes }).map(([label, value]) =>
+ value
+ ?
+ {label} |
+ {value} |
+
+ : ''
+ )}
+
+ );
+
+ return hoverItem;
+ }
+}
diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx
index 241183f..eed0475 100644
--- a/src/webview/memory-webview-view.tsx
+++ b/src/webview/memory-webview-view.tsx
@@ -38,10 +38,15 @@ import { AddressColumn } from './columns/address-column';
import { DataColumn } from './columns/data-column';
import { PrimeReactProvider } from 'primereact/api';
import 'primeflex/primeflex.css';
+import { hoverService, HoverService } from './hovers/hover-service';
+import { AddressHover } from './hovers/address-hover';
+import { DataHover } from './hovers/data-hover';
+import { VariableHover } from './hovers/variable-hover';
export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration {
title: string;
decorations: Decoration[];
+ hovers: HoverService;
columns: ColumnStatus[];
isFrozen: boolean;
}
@@ -64,6 +69,9 @@ class App extends React.Component<{}, MemoryAppState> {
columnContributionService.register(variableDecorator);
columnContributionService.register(new AsciiColumn());
decorationService.register(variableDecorator);
+ hoverService.register(new AddressHover());
+ hoverService.register(new DataHover());
+ hoverService.register(new VariableHover());
this.state = {
title: 'Memory',
memory: undefined,
@@ -71,6 +79,7 @@ class App extends React.Component<{}, MemoryAppState> {
offset: 0,
count: 256,
decorations: [],
+ hovers: hoverService,
columns: columnContributionService.getColumns(),
isMemoryFetching: false,
isFrozen: false,
@@ -91,11 +100,16 @@ class App extends React.Component<{}, MemoryAppState> {
messenger.sendNotification(readyType, HOST_EXTENSION, undefined);
}
+ public componentDidUpdate(): void {
+ hoverService.setMemoryState(this.state);
+ }
+
public render(): React.ReactNode {
return
((result, current, index) => {
if (index > 0) { result.push(', '); }
- result.push(React.createElement('span', { style: { color: current.color }, key: current.variable.name }, current.variable.name));
+ result.push(React.createElement(
+ 'span',
+ {
+ style: { color: current.color },
+ key: current.variable.name,
+ className: 'hoverable',
+ 'data-column': 'variables',
+ 'data-variables': stringifyWithBigInts(current.variable)
+ },
+ current.variable.name
+ ));
return result;
}, []);
}
@@ -130,4 +140,9 @@ export class VariableDecorator implements ColumnContribution, Decorator {
}
}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function stringifyWithBigInts(object: any): any {
+ return JSON.stringify(object, (_key, value) => typeof value === 'bigint' ? value.toString() : value);
+}
+
export const variableDecorator = new VariableDecorator();
diff --git a/tsconfig.json b/tsconfig.json
index 639c77a..8325d16 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,10 +9,11 @@
"esModuleInterop": true,
"jsx": "react",
"lib": [
+ "es2020",
"dom"
]
},
"include": [
"src"
]
-}
+}
\ No newline at end of file