diff --git a/components/Editor/InstructionsTable.tsx b/components/Editor/InstructionsTable.tsx index 9d39faa..103bc72 100644 --- a/components/Editor/InstructionsTable.tsx +++ b/components/Editor/InstructionsTable.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useRef } from 'react' +import React, { memo, useCallback, useEffect, useRef } from 'react' import cn from 'classnames' import ReactTooltip from 'react-tooltip' +import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso' import { IInstruction } from 'types' import { CodeType } from 'context/appUiContext' @@ -23,28 +24,49 @@ export const InstructionsTable = ({ variables: SierraVariables codeType: CodeType }) => { + // reference to the virtuoso instance + const virtuosoRef = useRef(null) + // reference to the range of items rendered in the dom by virtuoso + // to determine when to do smooth scroll and when to not + // Refer: https://virtuoso.dev/scroll-to-index/ + const virtuosoVisibleRange = useRef({ + startIndex: 0, + endIndex: 0, + }) useEffect(() => { - if (tableRef.current) { - const activeRowRef = rowRefs.current[activeIndexes[0]] - if (activeRowRef) { - tableRef.current?.scrollTo({ - top: activeRowRef.offsetTop - 58, - behavior: 'smooth', + if (virtuosoRef.current) { + const indexToScroll = activeIndexes[0] + if (indexToScroll !== undefined) { + const { startIndex, endIndex } = virtuosoVisibleRange.current + // check if the index is between our virtuoso range + // if within the range we do smooth scroll or else jump scroll + // Why? because of performance reasons. + + const behavior = + indexToScroll >= startIndex && indexToScroll <= endIndex + ? 'smooth' + : 'auto' + + // scroll to the index + virtuosoRef.current.scrollToIndex({ + index: indexToScroll, + align: 'center', + behavior: behavior, }) } } }, [activeIndexes]) - const tableRef = useRef(null) - const rowRefs = useRef>([]) - - const splitInLines = (instructionName: string) => - instructionName.split('\n').map((line, i) => ( - - {i !== 0 &&
} - {line} -
- )) + const splitInLines = useCallback( + (instructionName: string) => + instructionName.split('\n').map((line, i) => ( + + {i !== 0 &&
} + {line} +
+ )), + [], + ) const getRandomToolTipId = () => `tooltip-sierra-${Math.floor(Math.random() * 100000000)}` @@ -59,102 +81,164 @@ export const InstructionsTable = ({ return match[1] } - const formatSierraVariableValue = (values: Array): string => { - // TODO if type info is provided by the back-end - // => convert the array of felt252 to a more human-readable - // value, according to the variable type. - if (values.length > 1) { - return `[${values.join(', ')}]` - } - return values[0] - } - - const renderSierraVariableWithToolTip = ( - sierraVariableTag: string, - key: number, - ) => { - const variableName = getSierraVariableNameFromTag(sierraVariableTag) - if (!variableName) { - return sierraVariableTag - } - - if (!(variableName in variables) || variables[variableName].length === 0) { - return sierraVariableTag - } - - const variableValues = variables[variableName] - const tooltipId = getRandomToolTipId() - - return ( - - - {sierraVariableTag} - - - {formatSierraVariableValue(variableValues)} - - - ) - } - - const addVariableToolTipToSierraInstruction = (instructionName: string) => { - // regex to split on [*] and \n - const re = /(?=\[\d+\]|\n)|(?<=\[\d+\]|\n)/g - const parts = instructionName.split(re) + const formatSierraVariableValue = useCallback( + (values: Array): string => { + // TODO if type info is provided by the back-end + // => convert the array of felt252 to a more human-readable + // value, according to the variable type. + if (values.length > 1) { + return `[${values.join(', ')}]` + } + return values[0] + }, + [], + ) - return parts.map((part, index) => { - if (part === '\n') { - return
+ const renderSierraVariableWithToolTip = useCallback( + (sierraVariableTag: string, key: number) => { + const variableName = getSierraVariableNameFromTag(sierraVariableTag) + if (!variableName) { + return sierraVariableTag } - if (isSierraVariable(part)) { - return renderSierraVariableWithToolTip(part, index) + if ( + !(variableName in variables) || + variables[variableName].length === 0 + ) { + return sierraVariableTag } - return part - }) - } + const variableValues = variables[variableName] + const tooltipId = getRandomToolTipId() + + return ( + + + {sierraVariableTag} + + + {formatSierraVariableValue(variableValues)} + + + ) + }, + [formatSierraVariableValue, variables], + ) + + const addVariableToolTipToSierraInstruction = useCallback( + (instructionName: string) => { + // regex to split on [*] and \n + const re = /(?=\[\d+\]|\n)|(?<=\[\d+\]|\n)/g + const parts = instructionName.split(re) + + return parts.map((part, index) => { + if (part === '\n') { + return
+ } + + if (isSierraVariable(part)) { + return renderSierraVariableWithToolTip(part, index) + } + + return part + }) + }, + [renderSierraVariableWithToolTip], + ) return ( -
- - - {instructions.map((instruction, index) => { - const isActive = activeIndexes.includes(index) - const isError = errorIndexes.includes(index) - return ( - (rowRefs.current[index] = el)} - key={index} - className={cn( - 'border-b border-gray-200 dark:border-black-500', - { - 'text-gray-900 dark:text-gray-200': isActive, - 'text-gray-400 dark:text-gray-600': !isActive, - 'bg-red-100 dark:bg-red-500/10': isError, - }, - )} - > - - - - ) - })} - -
- {index + 1} - - {isActive && codeType === CodeType.Sierra - ? addVariableToolTipToSierraInstruction(instruction.name) - : splitInLines(instruction.name)} -
+
+ {/* + Some References for react-virtuoso: + Official Doc: https://virtuoso.dev/ + TableVirtuoso: https://virtuoso.dev/hello-table/ + components reference: https://virtuoso.dev/virtuoso-api/interfaces/TableComponents/, https://virtuoso.dev/footer/ + */} + (virtuosoVisibleRange.current = range)} + itemContent={(index, instruction, context) => { + const isActive = context?.activeIndexes?.includes(index) + // this should only return the content which should be inside tag of each row + // for and tag refer Table and TableRow components at bottom of this file + return ( + + ) + }} + /> ) } + +const Table = (props: any) => { + return
+} + +const TableRow = (props: any) => { + const { context, ...rest } = props + const { activeIndexes, errorIndexes } = context + const isActive = activeIndexes?.includes(rest?.['data-item-index']) + const isError = errorIndexes.includes(rest?.['data-item-index']) + return ( + + ) +} + +const TableRowContent = memo( + ({ + codeType, + index, + instruction, + isActive, + addVariableToolTipToSierraInstruction, + splitInLines, + }: { + index: number + isActive: boolean + instruction: IInstruction + codeType: CodeType + splitInLines: (instructionName: string) => React.JSX.Element[] + addVariableToolTipToSierraInstruction: ( + instructionName: string, + ) => (string | React.JSX.Element)[] + }) => { + return ( + <> + + + + ) + }, +) diff --git a/components/Tracer/index.tsx b/components/Tracer/index.tsx index 8f7aa87..a2a2ea2 100644 --- a/components/Tracer/index.tsx +++ b/components/Tracer/index.tsx @@ -1,7 +1,15 @@ -import { useContext, useEffect, useState, useRef, useReducer } from 'react' +import { + useContext, + useEffect, + useState, + useRef, + useReducer, + memo, +} from 'react' import cn from 'classnames' import { Priority, useRegisterActions } from 'kbar' +import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso' import { CairoVMApiContext, @@ -189,7 +197,7 @@ export const Tracer = () => {
void errorTraceEntry?: TraceEntry | null }) { - const { pc, ap, fp } = currentTraceEntry - const errorPc = errorTraceEntry?.pc || 0 + // reference to the virtuoso instance + const virtuosoRef = useRef(null) + // rederence to the range of items rendered in the dom by virtuoso + // to determine when to do smooth scroll and when to not + // Refer: https://virtuoso.dev/scroll-to-index/ + const virtuosoVisibleRange = useRef({ + startIndex: 0, + endIndex: 0, + }) + const { pc } = currentTraceEntry + + useEffect(() => { + if (virtuosoRef.current) { + const indexToScroll = currentFocus - 1 + if (indexToScroll !== undefined) { + const { startIndex, endIndex } = virtuosoVisibleRange.current + // check if the index is between our virtuoso range + // if within the range we do smooth scroll or else jump scroll + // Why? because of performance reasons. + + const behavior = + indexToScroll >= startIndex && indexToScroll <= endIndex + ? 'smooth' + : 'auto' + + // scroll to the index + virtuosoRef.current.scrollToIndex({ + index: indexToScroll, + align: 'center', + behavior: behavior, + }) + } + } + }, [currentTraceEntry, currentFocus]) - const [hoveredAddr, setHoveredAddr] = useState('') + const errorPc = errorTraceEntry?.pc || 0 return ( -
+ {index + 1} + + {isActive && codeType === CodeType.Sierra + ? addVariableToolTipToSierraInstruction(instruction.name) + : splitInLines(instruction.name)} +
- - - - - - - - - - - - - - - - - - - - - {Object.keys(memory).map((addr) => { - const isCurrent = pc.toString() == addr - const isError = errorPc.toString() == addr - const addrNum = Number(addr) - const isFocus = currentFocus == addrNum + <> + (virtuosoVisibleRange.current = range)} + itemContent={(index, addr) => { const hasBreakpoint = breakpoints[addr] + // this should only return the content which should be inside tag of each row + // for
memoryopcodeoff0off1off2dstop0op1respc_updateap_updatefp_update
and tag refer Table and TableRow components at bottom of this file return ( - setHoveredAddr(addr)} - onMouseLeave={() => setHoveredAddr('')} - > - - - - - {pcInstMap[addr] && ( - <> - - - - - - - - - - - - - )} - + ) - })} - -
- - {addrNum === pc && ( - [pc] - )} - {addrNum === ap && ( - [ap] - )} - {addrNum === fp && [fp]} - {addr}{memory[addr]}{pcInstMap[addr].opcode}{pcInstMap[addr].off0}{pcInstMap[addr].off1}{pcInstMap[addr].off2}{pcInstMap[addr].dst_register}{pcInstMap[addr].op0_register}{pcInstMap[addr].op1_addr}{pcInstMap[addr].res}{pcInstMap[addr].pc_update}{pcInstMap[addr].ap_update}{pcInstMap[addr].fp_update}
+ }} + /> + + ) +} + +const Table = (props: any) => { + return +} + +const TableRow = (props: any) => { + const { context, item: addr, ...rest } = props + const { pc, errorPc } = context + const isCurrent = pc.toString() == addr + const isError = errorPc.toString() == addr + return ( + + ) +} + +const TableRowContent = memo( + ({ + memory, + pcInstMap, + toogleBreakPoint, + addr, + hasBreakpoint, + currentTraceEntry, + }: { + memory: TracerData['memory'] + pcInstMap: TracerData['pcInstMap'] + toogleBreakPoint: (addr: string) => void + addr: string + hasBreakpoint: boolean + currentTraceEntry: TraceEntry + }) => { + const addrNum = Number(addr) + const { pc, ap, fp } = currentTraceEntry + return ( + <> + + + + + {pcInstMap[addr] && ( + <> + + + + + + + + + + + + + )} + + ) + }, +) + +const FixedHeader = () => { + return ( + // + // as default 'table-layout' property of our table element is set to default, it automatically + // sets the column width based on the maximum width of the content inside that column + // since we dont know what content will be inside the 'td' before hand + // table column widths were glitching while scrolling down as suddenly a big width content might come + // which used to change width of that whole column and resulting in a layout shift of the table + // + // Solution - + // added fixed width in each column + // set table-layout = 'fixed' css property in our table element check 'Table' component above + // + + + + + + + + + + + + + + + + + ) } diff --git a/package-lock.json b/package-lock.json index eeef37c..1a3c349 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "react-table": "^7.8.0", "react-tooltip": "^4.2.21", "react-use-id": "^1.0.0", + "react-virtuoso": "^4.7.7", "vercel": "^33.2.0", "wrangler": "^3.24.0" }, @@ -10719,6 +10720,18 @@ "react": "^16.6.3 || ^17.0.0" } }, + "node_modules/react-virtuoso": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.7.7.tgz", + "integrity": "sha512-n9NdMNaAtxHYH6e3H6zr1Kb08sp+1XPVnfE4cEMgrvmPBLugd9eeJtQo/1uA+SHhGaPX3uqZOuQsfKbX1r8P/A==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16 || >=17 || >= 18", + "react-dom": ">=16 || >=17 || >= 18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "dev": true, diff --git a/package.json b/package.json index a9d1a37..90c6897 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "react-table": "^7.8.0", "react-tooltip": "^4.2.21", "react-use-id": "^1.0.0", + "react-virtuoso": "^4.7.7", "vercel": "^33.2.0", "wrangler": "^3.24.0" },
+ + {addrNum === pc && [pc]} + {addrNum === ap && [ap]} + {addrNum === fp && [fp]} + {addr}{memory[addr]}{pcInstMap[addr].opcode}{pcInstMap[addr].off0}{pcInstMap[addr].off1}{pcInstMap[addr].off2}{pcInstMap[addr].dst_register}{pcInstMap[addr].op0_register}{pcInstMap[addr].op1_addr}{pcInstMap[addr].res}{pcInstMap[addr].pc_update}{pcInstMap[addr].ap_update}{pcInstMap[addr].fp_update}
memoryopcodeoff0off1off2dstop0op1respc_updateap_updatefp_update