Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hovers using PrimeReact Tooltip #87

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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}
gbodeen marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}