diff --git a/media/memory-table.css b/media/memory-table.css index 30e2911..9c1e55c 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); +} + +/* Specialized colors for the address-hover fields */ +.memory-hover .address-hover .primary { + background-color: var(--vscode-list-hoverBackground); +} + +/* Specialized colors for the variable-hover fields */ +.memory-hover table caption { + color: var(--vscode-symbolIcon-variableForeground); +} +.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..69d0799 --- /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..fb68a98 --- /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 = ( + + + {Object.entries({ type, start, end, words, bytes }).map(([label, value]) => + value + ? + + + + : '' + )} +
{name}
{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