mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
179 lines
4.5 KiB
TypeScript
179 lines
4.5 KiB
TypeScript
import type {FlamegraphFrame} from '../flamegraphFrame';
|
|
|
|
export type DirectionX = 'left' | 'right';
|
|
export type DirectionY = 'up' | 'down';
|
|
export type Direction = DirectionY | DirectionX;
|
|
/**
|
|
* selectNearestFrame walks a FlamegraphFrame tree the direction specified and returns the nearest
|
|
* FlamegraphFrame relative to the starting node.
|
|
*/
|
|
export function selectNearestFrame(frame: FlamegraphFrame, direction: Direction) {
|
|
if (direction === 'up') {
|
|
const parent = frame.parent;
|
|
|
|
// sentry root is a virtual root that should not be selectable
|
|
if (parent?.frame.isRoot) {
|
|
return frame;
|
|
}
|
|
|
|
// if there is an immediate parent, goto parent
|
|
if (parent) {
|
|
return parent;
|
|
}
|
|
|
|
// if there is no parent, we attempt to move left to the next stack
|
|
const leftSibling = getSibling(frame, 'left');
|
|
if (leftSibling) {
|
|
return leftSibling;
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
if (direction === 'down') {
|
|
// if there is an immediate child, goto child
|
|
const child = frame.children?.[0];
|
|
if (child) {
|
|
return child;
|
|
}
|
|
|
|
// if there is no child, we attempt to move right to the next stack
|
|
const sibling = scanForNearestSibling(frame, 'right');
|
|
if (sibling) {
|
|
return sibling;
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
// scan for the nearest sibling in either the left or right direction
|
|
// this function will walk us up the tree to a new branch
|
|
const sibling = scanForNearestSibling(frame, direction);
|
|
|
|
// if we find an adjacent sibling we attempt to walk down to a depth that
|
|
// matches our targetDepth
|
|
if (sibling) {
|
|
const targetDepth = frame.depth;
|
|
const nodeWithDepth = scanForNearestFrameWithDepth(sibling, targetDepth, direction);
|
|
if (nodeWithDepth) {
|
|
return nodeWithDepth;
|
|
}
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
/**
|
|
* scanForNearestSibling will walk up a branch looking for an adjacent sibling
|
|
*/
|
|
function scanForNearestSibling(frame: FlamegraphFrame, directionX: DirectionX) {
|
|
let node = frame;
|
|
while (node) {
|
|
const sibling = getSibling(node, directionX);
|
|
|
|
if (sibling) {
|
|
return sibling;
|
|
}
|
|
|
|
if (!node.parent) {
|
|
break;
|
|
}
|
|
|
|
node = node.parent;
|
|
}
|
|
return frame;
|
|
}
|
|
|
|
/**
|
|
* scanForNearestFrameWithDepth will walk down a FlamegraphFrame looking for a target depth.
|
|
* it will follow either the furthest left or right branch based on direction.
|
|
* if target depth is not found, we return the frame with the max depth along that branch.
|
|
*/
|
|
function scanForNearestFrameWithDepth(
|
|
frame: FlamegraphFrame,
|
|
depth: number,
|
|
directionX: DirectionX
|
|
) {
|
|
let node = frame;
|
|
let nextNode = node.children[directionX === 'right' ? 0 : node.children.length - 1];
|
|
|
|
while (node && nextNode) {
|
|
if (node.depth === depth) {
|
|
return node;
|
|
}
|
|
|
|
nextNode = node.children[directionX === 'right' ? 0 : node.children.length - 1];
|
|
if (nextNode) {
|
|
node = nextNode;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function getSibling(frame: FlamegraphFrame, directionX: DirectionX) {
|
|
const parent = frame.parent;
|
|
if (!parent) {
|
|
return null;
|
|
}
|
|
|
|
// get the index of the current frame relative to its siblings
|
|
const indexOfFrame = parent.children.indexOf(frame);
|
|
// this should never happen, it would make our data structure invalid
|
|
if (indexOfFrame === -1) {
|
|
throw Error('frame.parent.children does not include current frame');
|
|
}
|
|
|
|
if (directionX === 'right') {
|
|
const hasRightSiblings = indexOfFrame < parent.children.length - 1;
|
|
if (hasRightSiblings) {
|
|
return parent.children[indexOfFrame + 1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const hasLeftSiblings = indexOfFrame > 0;
|
|
if (hasLeftSiblings) {
|
|
return parent.children[indexOfFrame - 1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// loosely based spreadsheet navigation
|
|
const keyDirectionMap: Record<string, Direction> = {
|
|
ArrowUp: 'up',
|
|
ArrowDown: 'down',
|
|
ArrowLeft: 'left',
|
|
ArrowRight: 'right',
|
|
Tab: 'down',
|
|
'Shift+Tab': 'up',
|
|
} as const;
|
|
|
|
export function handleFlamegraphKeyboardNavigation(
|
|
evt: KeyboardEvent,
|
|
currentFrame: FlamegraphFrame | undefined,
|
|
inverted: boolean = false
|
|
) {
|
|
if (!currentFrame) {
|
|
return null;
|
|
}
|
|
|
|
const key = evt.shiftKey ? `Shift+${evt.key}` : evt.key;
|
|
let direction = keyDirectionMap[key];
|
|
|
|
// if the key is not mapped, don't handle it
|
|
if (!direction) {
|
|
return null;
|
|
}
|
|
if (inverted && (direction === 'up' || direction === 'down')) {
|
|
direction = direction === 'up' ? 'down' : 'up';
|
|
}
|
|
const nextSelection = selectNearestFrame(currentFrame, direction);
|
|
if (nextSelection === currentFrame) {
|
|
return null;
|
|
}
|
|
evt.preventDefault();
|
|
return nextSelection;
|
|
}
|