From bd8677a68208e162dec0de1a0a135add07090a26 Mon Sep 17 00:00:00 2001 From: Gabriel Bodeen Date: Tue, 9 Jan 2024 21:51:59 +0100 Subject: [PATCH 1/2] Hovers using PrimeReact Tooltip --- media/memory-table.css | 56 +++++++++++ src/plugin/adapter-registry/c-tracker.ts | 1 + src/webview/columns/address-column.tsx | 2 +- src/webview/columns/data-column.tsx | 4 +- src/webview/components/memory-table.tsx | 37 ++++++- src/webview/components/memory-widget.tsx | 3 + src/webview/hovers/address-hover.tsx | 89 +++++++++++++++++ src/webview/hovers/data-hover.tsx | 51 ++++++++++ src/webview/hovers/hover-service.tsx | 98 +++++++++++++++++++ src/webview/hovers/variable-hover.tsx | 52 ++++++++++ src/webview/memory-webview-view.tsx | 11 +++ src/webview/variables/variable-decorations.ts | 11 ++- tsconfig.json | 3 +- 13 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 src/webview/hovers/address-hover.tsx create mode 100644 src/webview/hovers/data-hover.tsx create mode 100644 src/webview/hovers/hover-service.tsx create mode 100644 src/webview/hovers/variable-hover.tsx diff --git a/media/memory-table.css b/media/memory-table.css index 8dfd910..8253d91 100644 --- a/media/memory-table.css +++ b/media/memory-table.css @@ -97,3 +97,59 @@ margin-top: 32px !important; /* avoid overlap with top 'Load more' widget */ width: 2px; } + +.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 ec84f0c..4c23c83 100644 --- a/src/webview/columns/address-column.tsx +++ b/src/webview/columns/address-column.tsx @@ -31,7 +31,7 @@ export class AddressColumn implements ColumnContribution { render(range: BigIntMemoryRange, _: Memory, options: TableRenderOptions): ReactNode { return {options.showRadixPrefix && {getRadixMarker(options.addressRadix)}} - {getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)} + {getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)} ; } } diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx index 5cb847f..0389b3c 100644 --- a/src/webview/columns/data-column.tsx +++ b/src/webview/columns/data-column.tsx @@ -47,11 +47,11 @@ export class DataColumn implements ColumnContribution { this.applyEndianness(words, options); const isLast = address + 1n >= range.endAddress; const style: React.CSSProperties | undefined = isLast ? undefined : this.byteGroupStyle; - 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 a328828..bedf60f 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -19,6 +19,7 @@ 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'; @@ -29,6 +30,8 @@ import { DataColumn } from '../columns/data-column'; import { createColumnVscodeContext, createSectionVscodeContext } from '../utils/vscode-contexts'; import { WebviewSelection } from '../../common/messaging'; import { debounce } from 'lodash'; +import type { HoverService } from '../hovers/hover-service'; +import { TooltipEvent } from 'primereact/tooltip/tooltipoptions'; export interface MoreMemorySelectProps { activeReadArguments: Required; @@ -130,6 +133,7 @@ interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguratio memory?: Memory; decorations: Decoration[]; effectiveAddressLength: number; + hoverService: HoverService; fetchMemory(partialOptions?: Partial): Promise; isMemoryFetching: boolean; isFrozen: boolean; @@ -157,6 +161,7 @@ interface MemoryTableState { */ groupsPerRowToRender: number; selection: MemoryTableCellSelection | null; + hoverContent: React.ReactNode; } export type MemorySizeOptions = Pick & { groupsPerRow: number }; @@ -196,6 +201,7 @@ export class MemoryTable extends React.PureComponent, }; } @@ -276,7 +282,15 @@ export class MemoryTable extends React.PureComponent +
+ {this.state.hoverContent} ref={this.datatableRef} onContextMenuCapture={this.onContextMenu} @@ -558,6 +572,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.hoverService.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 4de9070..71f4c7d 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -23,6 +23,7 @@ import { OptionsWidget } from './options-widget'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; import { VscodeContext, createAppVscodeContext } from '../utils/vscode-contexts'; import { WebviewSelection } from '../../common/messaging'; +import { HoverService } from '../hovers/hover-service'; interface MemoryWidgetProps extends MemoryDisplayConfiguration { messageParticipant: WebviewIdMessageParticipant; @@ -31,6 +32,7 @@ interface MemoryWidgetProps extends MemoryDisplayConfiguration { memory?: Memory; title: string; decorations: Decoration[]; + hoverService: HoverService; columns: ColumnStatus[]; effectiveAddressLength: number; isMemoryFetching: boolean; @@ -98,6 +100,7 @@ export class MemoryWidget extends React.Component candidate.active)} memory={this.props.memory} endianness={this.props.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 = ( + + + {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 b3da016..f410aea 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -43,12 +43,17 @@ import { PrimeReactProvider } from 'primereact/api'; import 'primeflex/primeflex.css'; import { getAddressLength, getAddressString } from '../common/memory-range'; import { Endianness } from '../common/memory-range'; +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 { messageParticipant: WebviewIdMessageParticipant; title: string; effectiveAddressLength: number; decorations: Decoration[]; + hoverService: HoverService; columns: ColumnStatus[]; isFrozen: boolean; } @@ -79,6 +84,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 = { messageParticipant: { type: 'webview', webviewId: '' }, title: 'Memory', @@ -87,6 +95,7 @@ class App extends React.Component<{}, MemoryAppState> { configuredReadArguments: DEFAULT_READ_ARGUMENTS, activeReadArguments: DEFAULT_READ_ARGUMENTS, decorations: [], + hoverService: hoverService, columns: columnContributionService.getColumns(), isMemoryFetching: false, isFrozen: false, @@ -121,6 +130,7 @@ class App extends React.Component<{}, MemoryAppState> { this.setState({ effectiveAddressLength }); } } + hoverService.setMemoryState(this.state); } public render(): React.ReactNode { @@ -132,6 +142,7 @@ class App extends React.Component<{}, MemoryAppState> { activeReadArguments={this.state.activeReadArguments} memory={this.state.memory} decorations={this.state.decorations} + hoverService={this.state.hoverService} columns={this.state.columns} title={this.state.title} effectiveAddressLength={this.state.effectiveAddressLength} diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index 291d657..98c9fc0 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -81,7 +81,11 @@ export class VariableDecorator implements ColumnContribution, Decorator { return this.getVariablesInRange(range)?.reduce((result, current, index) => { if (index > 0) { result.push(', '); } result.push(React.createElement('span', { - style: { color: current.color }, key: current.variable.name, + style: { color: current.color }, + key: current.variable.name, + className: 'hoverable', + 'data-column': 'variables', + 'data-variables': stringifyWithBigInts(current.variable), ...createVariableVscodeContext(current.variable) }, current.variable.name)); return result; @@ -134,4 +138,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 From a01d06ee6097ac3115d6f1953eea3d3b1f00cf65 Mon Sep 17 00:00:00 2001 From: Gabriel Bodeen Date: Fri, 8 Mar 2024 21:53:53 +0100 Subject: [PATCH 2/2] shorter wait; hover underline; hoverable radix --- media/memory-table.css | 4 ++-- src/webview/columns/address-column.tsx | 4 ++-- src/webview/components/memory-table.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/media/memory-table.css b/media/memory-table.css index 8253d91..788f971 100644 --- a/media/memory-table.css +++ b/media/memory-table.css @@ -98,8 +98,8 @@ width: 2px; } -.hoverable { - position: relative; +.hoverable:hover { + border-bottom: 1px dotted var(--vscode-editorHoverWidget-border); } /* Basic hover formatting (copied from Monaco hovers) */ diff --git a/src/webview/columns/address-column.tsx b/src/webview/columns/address-column.tsx index 4c23c83..ccbc3fd 100644 --- a/src/webview/columns/address-column.tsx +++ b/src/webview/columns/address-column.tsx @@ -29,9 +29,9 @@ export class AddressColumn implements ColumnContribution { fittingType: ColumnFittingType = 'content-width'; render(range: BigIntMemoryRange, _: Memory, options: TableRenderOptions): ReactNode { - return + return {options.showRadixPrefix && {getRadixMarker(options.addressRadix)}} - {getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)} + {getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)} ; } } diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index bedf60f..908d2f2 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -288,7 +288,7 @@ export class MemoryTable extends React.PureComponent{this.state.hoverContent}