Skip to content

Commit

Permalink
Hovers using PrimeReact Tooltip (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
gbodeen committed Mar 12, 2024
1 parent ed4cdff commit 3cd4184
Show file tree
Hide file tree
Showing 13 changed files with 412 additions and 6 deletions.
56 changes: 56 additions & 0 deletions media/memory-table.css
Expand Up @@ -97,3 +97,59 @@
margin-top: 32px !important; /* avoid overlap with top 'Load more' widget */
width: 2px;
}

.hoverable:hover {
border-bottom: 1px dotted var(--vscode-editorHoverWidget-border);
}

/* 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);
}
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 {
fittingType: ColumnFittingType = 'content-width';

render(range: BigIntMemoryRange, _: Memory, options: TableRenderOptions): ReactNode {
return <span className='memory-start-address'>
return <span className='memory-start-address hoverable' data-column='address'>
{options.showRadixPrefix && <span className='radix-prefix'>{getRadixMarker(options.addressRadix)}</span>}
<span className='address'>{getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)}</span>
</span>;
Expand Down
4 changes: 2 additions & 2 deletions src/webview/columns/data-column.tsx
Expand Up @@ -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(<span className='byte-group' style={style} key={address.toString(16)}>{words}</span>);
groups.push(<span className='byte-group hoverable' data-column='data' style={style} key={address.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
37 changes: 36 additions & 1 deletion src/webview/components/memory-table.tsx
Expand Up @@ -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';
Expand All @@ -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<DebugProtocol.ReadMemoryArguments>;
Expand Down Expand Up @@ -130,6 +133,7 @@ interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguratio
memory?: Memory;
decorations: Decoration[];
effectiveAddressLength: number;
hoverService: HoverService;
fetchMemory(partialOptions?: Partial<DebugProtocol.ReadMemoryArguments>): Promise<void>;
isMemoryFetching: boolean;
isFrozen: boolean;
Expand Down Expand Up @@ -157,6 +161,7 @@ interface MemoryTableState {
*/
groupsPerRowToRender: number;
selection: MemoryTableCellSelection | null;
hoverContent: React.ReactNode;
}

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

Expand Down Expand Up @@ -276,7 +282,15 @@ 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'>
<Tooltip
// On mouseover of a .hoverable element, this.handleOnBeforeTooltipShow runs and generates this.state.hoverContent
target=".hoverable"
onBeforeShow={this.handleOnBeforeTooltipShow}
onHide={this.handleOnTooltipHide}
showDelay={300}
autoHide={false}
>{this.state.hoverContent}</Tooltip>
<DataTable<MemoryRowData[]>
ref={this.datatableRef}
onContextMenuCapture={this.onContextMenu}
Expand Down Expand Up @@ -558,6 +572,27 @@ export class MemoryTable extends React.PureComponent<MemoryTableProps, MemoryTab
const textSelection = window.getSelection()?.toString() ?? '';
return this.state.selection ? { textSelection, selectedCell: { column: this.state.selection.field, value: this.state.selection.textContent } } : { textSelection };
}

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.hoverService.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 @@ -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;
Expand All @@ -31,6 +32,7 @@ interface MemoryWidgetProps extends MemoryDisplayConfiguration {
memory?: Memory;
title: string;
decorations: Decoration[];
hoverService: HoverService;
columns: ColumnStatus[];
effectiveAddressLength: number;
isMemoryFetching: boolean;
Expand Down Expand Up @@ -98,6 +100,7 @@ export class MemoryWidget extends React.Component<MemoryWidgetProps, MemoryWidge
configuredReadArguments={this.props.configuredReadArguments}
activeReadArguments={this.props.activeReadArguments}
decorations={this.props.decorations}
hoverService={this.props.hoverService}
columnOptions={this.props.columns.filter(candidate => candidate.active)}
memory={this.props.memory}
endianness={this.props.endianness}
Expand Down
89 changes: 89 additions & 0 deletions 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<React.ReactNode> {
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 = (
<table className='address-hover'>
{Object.entries({ binary, octal, decimal, hexadecimal, utf8 }).map(([label, value]) =>
value
? <tr className={`label-value-pair ${label === primaryRadix ? 'primary' : ''}`}>
<td className={`label ${label}`}>{label}</td>
<td className={`value ${label}`}>{value}</td>
</tr>
: ''
)}
</table>
);
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 = (
<table className='data-hover'>
{Object.entries({ binary, octal, decimal, hexadecimal, utf8 }).map(([label, value]) =>
value
? <tr className='label-value-pair'>
<td className={`label ${label}`}>{label}</td>
<td className={`value ${label}`}>{value}</td>
</tr>
: ''
)}
</table>
);
return hoverItem;
}
}

0 comments on commit 3cd4184

Please sign in to comment.