Skip to content

Commit

Permalink
Hovers using PrimeReact Tooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
gbodeen committed Feb 28, 2024
1 parent d1400a1 commit ecf9edd
Show file tree
Hide file tree
Showing 13 changed files with 374 additions and 5 deletions.
18 changes: 18 additions & 0 deletions media/memory-table.css
Expand Up @@ -83,3 +83,21 @@
.radix-prefix {
opacity: .6;
}

.hoverable {
position: relative;
}

.memory-hover {
min-width: fit-content;
max-width: var(--vscode-hover-maxWidth,500px);
padding: 5px;
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);
}
1 change: 1 addition & 0 deletions src/plugin/adapter-registry/c-tracker.ts
Expand Up @@ -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 : ''));
Expand Down
2 changes: 1 addition & 1 deletion src/webview/columns/address-column.tsx
Expand Up @@ -29,7 +29,7 @@ export class AddressColumn implements ColumnContribution {
render(range: BigIntMemoryRange, _: Memory, options: MemoryDisplayConfiguration): ReactNode {
return <span className='memory-start-address'>
{options.showRadixPrefix && <span className='radix-prefix'>{getRadixMarker(options.addressRadix)}</span>}
<span className='address'>{getAddressString(range.startAddress, options.addressRadix)}</span>
<span className='address hoverable' data-column='address'>{getAddressString(range.startAddress, options.addressRadix)}</span>
</span>;
}
}
4 changes: 2 additions & 2 deletions src/webview/columns/data-column.tsx
Expand Up @@ -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(<span className='byte-group' key={i.toString(16)}>{words}</span>);
groups.push(<span className='byte-group hoverable' data-column='data' key={i.toString(16)}>{words}</span>);
words = [];
}
}
if (words.length) { groups.push(<span className='byte-group' key={(range.endAddress - BigInt(words.length)).toString(16)}>{words}</span>); }
if (words.length) { groups.push(<span className='byte-group hoverable' data-column='data' key={(range.endAddress - BigInt(words.length)).toString(16)}>{words}</span>); }
return groups;
}

Expand Down
32 changes: 32 additions & 0 deletions src/webview/components/memory-table.tsx
Expand Up @@ -19,12 +19,15 @@ import memoize from 'memoize-one';
import { Column } from 'primereact/column';
import { DataTable, DataTableCellSelection, DataTableProps, DataTableRowData, 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;
Expand Down Expand Up @@ -96,6 +99,7 @@ export const MoreMemorySelect: React.FC<MoreMemorySelectProps> = ({ count, offse
interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguration {
memory?: Memory;
decorations: Decoration[];
hovers: HoverService;
offset: number;
count: number;
fetchMemory(partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void>;
Expand All @@ -116,6 +120,7 @@ interface MemoryRowData {

interface MemoryTableState {
selection: DataTableCellSelection<MemoryRowData[]> | null;
hoverContent: React.ReactNode;
}

type MemorySizeOptions = Pick<MemoryTableProps, 'bytesPerWord' | 'wordsPerGroup' | 'groupsPerRow'>;
Expand Down Expand Up @@ -148,6 +153,7 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
this.state = {
// eslint-disable-next-line no-null/no-null
selection: null,
hoverContent: <></>,
};
}

Expand Down Expand Up @@ -179,6 +185,11 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab

return (
<div className='flex-1 overflow-auto px-4'>
<Tooltip
onBeforeShow={this.handleOnBeforeTooltipShow}
onHide={this.handleOnTooltipHide}
target=".hoverable"
>{this.state.hoverContent}</Tooltip>
<DataTable<MemoryRowData[]>
ref={this.datatableRef}
{...props}
Expand Down Expand Up @@ -329,6 +340,27 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
endAddress: startAddress + memoryTableOptions.bigWordsPerRow
};
}

protected handleOnBeforeTooltipShow = async (event: TooltipEvent): Promise<void> => {
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 {
Expand Down
3 changes: 3 additions & 0 deletions src/webview/components/memory-widget.tsx
Expand Up @@ -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;
Expand Down Expand Up @@ -76,6 +78,7 @@ export class MemoryWidget extends React.Component<MemoryWidgetProps, MemoryWidge
/>
<MemoryTable
decorations={this.props.decorations}
hovers={this.props.hovers}
columnOptions={this.props.columns.filter(candidate => candidate.active)}
memory={this.props.memory}
endianness={this.state.endianness}
Expand Down
84 changes: 84 additions & 0 deletions src/webview/hovers/address-hover.tsx
@@ -0,0 +1,84 @@
/********************************************************************************
* 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<React.ReactNode> {
if (columnId !== 'address') { return; }

let binary = '';
let octal = '';
let decimal = '';
let hexadecimal = '';
let num = 0;

switch (addressRadix) {
case Radix.Binary:
binary = textContent;
num = parseInt(binary, 2);
octal = num.toString(8);
decimal = num.toString(10);
hexadecimal = num.toString(16);
break;
case Radix.Octal:
octal = textContent;
num = parseInt(octal, 8);
binary = num.toString(2);
decimal = num.toString(10);
hexadecimal = num.toString(16);
break;
case Radix.Decimal:
decimal = textContent;
num = parseInt(decimal, 10);
binary = num.toString(2);
octal = num.toString(8);
hexadecimal = num.toString(16);
break;
case Radix.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 = (
<div className='address-hover'>
{Object.entries({ binary, octal, decimal, hexadecimal, utf8 }).map(([label, value]) =>
value
? <div className='address-hover-pair'>
<span className='address-hover-type'>{label}: </span>
<span className='address-hover-value'>{value}</span>
</div>
: ''
)}
</div>
);
return hoverItem;
}
}
51 changes: 51 additions & 0 deletions 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<React.ReactNode> {
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 = (
<div className='data-hover'>
{Object.entries({ binary, octal, decimal, hexadecimal, utf8 }).map(([label, value]) =>
value
? <div className='data-hover-pair'>
<span className='data-hover-type'>{label}: </span>
<span className='data-hover-value'>{value}</span>
</div>
: ''
)}
</div>
);
return hoverItem;
}
}
98 changes: 98 additions & 0 deletions 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<React.ReactNode>;

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<React.ReactNode> {
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 <div className='memory-hover'>{nodes}</div>;
}
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();

0 comments on commit ecf9edd

Please sign in to comment.