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

Add search capabilities to Peripheral Inspector view #25

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/components/tree/components/search-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made available under the
* terms of the MIT License as outlined in the LICENSE File
********************************************************************************/

import { VSCodeButton } from '@vscode/webview-ui-toolkit/react';
import React from 'react';
import './search.css';

export interface SearchOverlayProps {
onChange?: (text: string) => void;
onShow?: () => void;
onHide?: () => void;
}

export interface SearchOverlay {
focus: () => void;
value(): string;
setValue: (value: string) => void;
show: () => void;
hide: () => void;
}

export const SearchOverlay = React.forwardRef<SearchOverlay, SearchOverlayProps>((props, ref) => {
const [showSearch, setShowSearch] = React.useState(false);
const searchTextRef = React.useRef<HTMLInputElement>(null);
const previousFocusedElementRef = React.useRef<HTMLElement | null>(null);

const show = () => {
previousFocusedElementRef.current = document.activeElement as HTMLElement;
setShowSearch(true);
setTimeout(() => searchTextRef.current?.select(), 100);
props.onShow?.();
};

const hide = () => {
setShowSearch(false);
props.onHide?.();
if (previousFocusedElementRef.current) {
previousFocusedElementRef.current.focus();
}
};

const onTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
props.onChange?.(value);
};

const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
e.stopPropagation();
show();
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
hide();
}
};

const onFocus = (e: React.FocusEvent<HTMLInputElement>) => {
if (e.relatedTarget) {
previousFocusedElementRef.current = e.relatedTarget as HTMLElement;
}
};

React.useImperativeHandle(ref, () => ({
focus: () => searchTextRef.current?.focus(),
value: () => searchTextRef.current?.value ?? '',
setValue: (newValue: string) => {
if (searchTextRef.current) {
searchTextRef.current.value = newValue;
}
},
show: () => show(),
hide: () => hide()
}));

return (<div className={showSearch ? 'search-overlay visible' : 'search-overlay'} onKeyDown={onKeyDown}>
<input ref={searchTextRef} onChange={onTextChange} onFocus={onFocus} placeholder="Find" className="search-input" />
<VSCodeButton title='Close (Escape)' appearance='icon' aria-label='Close (Escape)'><span className='codicon codicon-close' onClick={() => hide()} /></VSCodeButton>
</div>
);
});
80 changes: 80 additions & 0 deletions src/components/tree/components/search.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/********************************************************************************
* Copyright (C) 2024 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the MIT License as outlined in the LICENSE File
********************************************************************************/

.search-overlay {
position: fixed;
top: -33px;
opacity: 0;
right: 5px;
background-color: var(--vscode-editorWidget-background);
box-shadow: 0 0 4px 1px var(--vscode-widget-shadow);
color: var(--vscode-editorWidget-foreground);
border-bottom: 1px solid var(--vscode-widget-border);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-left: 1px solid var(--vscode-widget-border);
border-right: 1px solid var(--vscode-widget-border);
box-sizing: border-box;
height: 33px;
line-height: 19px;
overflow: hidden;
padding: 4px;
z-index: 35;
display: flex;
flex-direction: row;
gap: 5px;

-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-ms-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}

.search-overlay.visible {
top: 5px;
opacity: 1;
}

.search-overlay .search-input {
color: var(--vscode-input-foreground);
background-color: var(--vscode-input-background);
outline: none;
scrollbar-width: none;
border: none;
box-sizing: border-box;
display: inline-block;
font-family: inherit;
font-size: inherit;
height: 100%;
line-height: inherit;
resize: none;
width: 100%;
padding: 4px 6px;
margin: 0;
}

.search-overlay input.search-input:focus {
outline: 1px solid var(--vscode-focusBorder)
}


.search-input::placeholder {
color: var(--vscode-input-placeholderForeground);
}

.search-input::-moz-placeholder {
color: var(--vscode-input-placeholderForeground);
}

.search-input:-ms-input-placeholder {
color: var(--vscode-input-placeholderForeground);
}

.search-input:-webkit-input-placeholder {
color: var(--vscode-input-placeholderForeground);
}
50 changes: 38 additions & 12 deletions src/components/tree/components/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { classNames } from 'primereact/utils';
import React, { useEffect, useState } from 'react';
import { useCDTTreeContext } from '../tree-context';
import { CDTTreeItem, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types';
import { SearchOverlay } from './search-overlay';

import { createActions, createHighlightedText, createLabelWithTooltip } from './utils';
import { ProgressBar } from 'primereact/progressbar';

Expand All @@ -25,12 +27,14 @@ export type ComponentTreeProps = {

const PROGRESS_BAR_HIDE_DELAY = 200;

export const ComponentTree = (props: ComponentTreeProps) => {
export const ComponentTree = ({ nodes, selectedNode, isLoading }: ComponentTreeProps) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: expanding the properties here is inconsistent with other components in this repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I prefer deconstructing properties as it makes the code more readable in my opinion. I agree, however, that we should stay consistent and for the purpose of this PR I changed it back to accessing props directly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates, @martin-fleck-at !

Thanks for fixing the find box behavior. I still do see a minimal jump of the box by one or two pixels to the left if the scrollbar disappears. That is on Windows. Not overly concerned about that one though.

I can confirm the progress line is gone. Thanks!

I checked again the last item in the list. I have to rephrase the report:
I does find registers by names even if they are invisible due to collapsed groups. However, for bit fields, the behavior is as described.

An example is for the NXP FRDM-K32L3A6 board/SVD file (Cortex-M4):

  • Unfold: CoreDebug -> DHCSR_Read . You can see for example C_HALT.
    image
  • Search for C_HALT. You can see it appearing in the filtered tree.
    image
  • Now collapse any of the two parent groups. The filtered list empties.
    image

I don't know exactly what's involved to change this. Probably worth to address in a separate PR. If fixable with the chosen GUI library.

const treeContext = useCDTTreeContext();
const [showProgressBar, setShowProgressBar] = useState(false);
const [filter, setFilter] = React.useState<string | undefined>();
const searchRef = React.useRef<SearchOverlay>(null);

useEffect(() => {
if (!props.isLoading) {
if (!isLoading) {
// Delay hiding the progress bar to allow the animation to complete
const timer = setTimeout(() => {
setShowProgressBar(false);
Expand All @@ -39,20 +43,24 @@ export const ComponentTree = (props: ComponentTreeProps) => {
} else {
setShowProgressBar(true);
}
}, [props.isLoading]);
}, [isLoading]);

// Assemble the tree
if (props.nodes === undefined) {
if (nodes === undefined) {
return <div>loading</div>;
}

// Assemble the tree
if (nodes === undefined) {
return <div>
<ProgressBar mode="indeterminate" className='sticky top-0'></ProgressBar>
</div>;
}

if (!props.nodes.length) {
if (!nodes.length) {
return <div>No children provided</div>;
}


// Event handler
const onToggle = async (event: TreeEventNodeEvent) => {
if (event.node.leaf) {
Expand All @@ -70,9 +78,9 @@ export const ComponentTree = (props: ComponentTreeProps) => {
const nodeTemplate = (node: TreeNode) => {
CDTTreeItem.assert(node);
return <div className='tree-node'
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.path })}
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.data.path })}
>
{createLabelWithTooltip(createHighlightedText(node.label, node.options?.highlights), node.options?.tooltip)}
{createLabelWithTooltip(createHighlightedText(node.label, node.data.options?.highlights), node.data.options?.tooltip)}
{createActions(treeContext, node)}
</div>;
};
Expand All @@ -87,24 +95,42 @@ export const ComponentTree = (props: ComponentTreeProps) => {
</div>;
};

return <div>
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
e.stopPropagation();
searchRef.current?.show();
}
};

const onSearchShow = () => setFilter(searchRef.current?.value());
const onSearchHide = () => setFilter(undefined);
const onSearchChange = (text: string) => setFilter(text);

return <div onKeyDown={onKeyDown}>
<div className='progress-bar-container'>
{showProgressBar &&
<ProgressBar mode="indeterminate" className='sticky top-0'></ProgressBar>
}
</div>
<SearchOverlay key={'search'} ref={searchRef} onHide={onSearchHide} onShow={onSearchShow} onChange={onSearchChange} />
<Tree
value={props.nodes}
value={nodes}
className="w-full md:w-30rem"
style={{ minWidth: '10rem' }}
nodeTemplate={nodeTemplate}
togglerTemplate={togglerTemplate}
selectionMode='single'
selectionKeys={props.selectedNode?.key?.toString()}
selectionKeys={selectedNode?.key?.toString()}
onNodeClick={event => onClick(event)}
onExpand={event => onToggle(event)}
onCollapse={event => onToggle(event)}
filter={true}
filterMode='strict'
filterValue={filter}
onFilterValueChange={() => { /* needed as otherwise the filter value is not taken into account */ }}
showHeader={false}
/>
</div >;
</div>;
};

31 changes: 24 additions & 7 deletions src/components/tree/components/treetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import React, { useEffect, useState } from 'react';
import { useCDTTreeContext } from '../tree-context';
import { CDTTreeItem, CDTTreeTableColumnDefinition, CDTTreeTableExpanderColumn, CDTTreeTableStringColumn, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types';
import { createActions, createHighlightedText, createIcon, createLabelWithTooltip } from './utils';
import { SearchOverlay } from './search-overlay';
import { ProgressBar } from 'primereact/progressbar';

export type ComponentTreeTableProps = {
Expand All @@ -30,6 +31,8 @@ const PROGRESS_BAR_HIDE_DELAY = 200;
export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const treeContext = useCDTTreeContext();
const [showProgressBar, setShowProgressBar] = useState(false);
const [filter, setFilter] = React.useState<string | undefined>();
const searchRef = React.useRef<SearchOverlay>(null);

useEffect(() => {
if (!props.isLoading) {
Expand All @@ -54,7 +57,6 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
return <div>No children provided</div>;
}


// Event handler
const onToggle = (event: TreeTableEvent) => {
if (event.node.leaf) {
Expand All @@ -72,7 +74,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const template = (node: TreeNode, field: string) => {
CDTTreeItem.assert(node);

const column = node.columns?.[field];
const column = node.data.columns?.[field];

if (column?.type === 'expander') {
return expanderTemplate(node, column);
Expand All @@ -86,7 +88,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const expanderTemplate = (node: TreeNode, column: CDTTreeTableExpanderColumn) => {
CDTTreeItem.assert(node);

return <div style={{ paddingLeft: `${((node.path.length ?? 1)) * 8}px` }}
return <div style={{ paddingLeft: `${((node.data.path.length ?? 1)) * 8}px` }}
>
<div className='treetable-node' >
<div
Expand All @@ -107,7 +109,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const text = createHighlightedText(column.label, column.highlight);

return <div
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.path })}
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.data.path })}
>
{createLabelWithTooltip(text, column.tooltip)}
</div>;
Expand All @@ -126,12 +128,25 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const expandedState = getExpandedState(props.nodes);
const selectedKey = props.selectedNode ? props.selectedNode.key as string : undefined;

return <div>
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
e.stopPropagation();
searchRef.current?.show();
}
};

const onSearchShow = () => setFilter(searchRef.current?.value());
const onSearchHide = () => setFilter(undefined);
const onSearchChange = (text: string) => setFilter(text);

return <div onKeyDown={onKeyDown}>
<div className='progress-bar-container'>
{showProgressBar &&
<ProgressBar mode="indeterminate" className='sticky top-0'></ProgressBar>
}
</div>
<SearchOverlay key={'search'} ref={searchRef} onHide={onSearchHide} onShow={onSearchShow} onChange={onSearchChange} />
<TreeTable
value={props.nodes}
selectionKeys={selectedKey}
Expand All @@ -146,11 +161,13 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
onExpand={event => onToggle(event)}
onCollapse={event => onToggle(event)}
onRowClick={event => onClick(event)}
filterMode='strict' // continue searching on children
globalFilter={filter}
>
{props.columnDefinitions?.map(c => {
return <Column key={`${c.field}_column`} field={c.field} body={(node) => template(node, c.field)} expander={c.expander} />;
return <Column key={`${c.field}_column`} field={c.field} body={(node) => template(node, c.field)} expander={c.expander} filter={true} />;
})}
<Column field="actions" style={{ width: '64px' }} body={actionsTemplate} />
<Column key={'actions'} field="actions" style={{ width: '64px' }} body={actionsTemplate} />
</TreeTable>
</div>;
};
Expand Down
4 changes: 2 additions & 2 deletions src/components/tree/components/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function createHighlightedText(label?: string, highlights?: [number, numb
export function createLabelWithTooltip(child: React.JSX.Element, tooltip?: string): React.JSX.Element {
const label = <div className="tree-label flex-auto flex align-items-center">
{child}
</div >;
</div>;

if (tooltip === undefined) {
return label;
Expand All @@ -72,7 +72,7 @@ export function createActions(context: CDTTreeContext, node: TreeNode): React.JS
};

return <div className="tree-actions">
{node.options?.commands?.map(a => <i key={a.commandId} className={`codicon codicon-${a.icon}`} onClick={(event) => onClick(event, a)}></i>)}
{node.data.options?.commands?.map(a => <i key={a.commandId} className={`codicon codicon-${a.icon}`} onClick={(event) => onClick(event, a)}></i>)}
</div>;
}

Expand Down
Loading
Loading