Skip to content

Commit

Permalink
Merge pull request #135 from jaipaljadeja/feat/virtualized-rendering
Browse files Browse the repository at this point in the history
feat: Virtualized Rendering
  • Loading branch information
barabanovro authored Apr 5, 2024
2 parents 9192869 + 3b12c48 commit 511b5ef
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 194 deletions.
296 changes: 190 additions & 106 deletions components/Editor/InstructionsTable.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -23,28 +24,49 @@ export const InstructionsTable = ({
variables: SierraVariables
codeType: CodeType
}) => {
// reference to the virtuoso instance
const virtuosoRef = useRef<TableVirtuosoHandle>(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<HTMLDivElement>(null)
const rowRefs = useRef<Array<HTMLTableRowElement | null>>([])

const splitInLines = (instructionName: string) =>
instructionName.split('\n').map((line, i) => (
<span key={i}>
{i !== 0 && <br />}
{line}
</span>
))
const splitInLines = useCallback(
(instructionName: string) =>
instructionName.split('\n').map((line, i) => (
<span key={i}>
{i !== 0 && <br />}
{line}
</span>
)),
[],
)

const getRandomToolTipId = () =>
`tooltip-sierra-${Math.floor(Math.random() * 100000000)}`
Expand All @@ -59,102 +81,164 @@ export const InstructionsTable = ({
return match[1]
}

const formatSierraVariableValue = (values: Array<string>): 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 (
<React.Fragment key={key}>
<span
data-tip
data-for={tooltipId}
className="hover:text-orange-500 cursor-pointer"
>
{sierraVariableTag}
</span>
<ReactTooltip id={tooltipId} effect="solid">
<span>{formatSierraVariableValue(variableValues)}</span>
</ReactTooltip>
</React.Fragment>
)
}

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>): 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 <br key={index} />
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 (
<React.Fragment key={key}>
<span
data-tip
data-for={tooltipId}
className="hover:text-orange-500 cursor-pointer"
>
{sierraVariableTag}
</span>
<ReactTooltip id={tooltipId} effect="solid">
<span>{formatSierraVariableValue(variableValues)}</span>
</ReactTooltip>
</React.Fragment>
)
},
[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 <br key={index} />
}

if (isSierraVariable(part)) {
return renderSierraVariableWithToolTip(part, index)
}

return part
})
},
[renderSierraVariableWithToolTip],
)

return (
<div
ref={tableRef}
className="overflow-auto h-full pane pane-light relative bg-gray-50 dark:bg-black-600 border-gray-200 dark:border-black-500"
>
<table className="w-full font-mono text-tiny">
<tbody>
{instructions.map((instruction, index) => {
const isActive = activeIndexes.includes(index)
const isError = errorIndexes.includes(index)
return (
<tr
ref={(el) => (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,
},
)}
>
<td className={`pl-6 pr-1 px-2 whitespace-nowrap w-[1%]`}>
{index + 1}
</td>
<td className="py-1 px-2 max-w-40 break-words">
{isActive && codeType === CodeType.Sierra
? addVariableToolTipToSierraInstruction(instruction.name)
: splitInLines(instruction.name)}
</td>
</tr>
)
})}
</tbody>
</table>
<div className="h-full bg-gray-50 dark:bg-black-600 border-gray-200 dark:border-black-500">
{/*
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/
*/}
<TableVirtuoso
ref={virtuosoRef}
style={{ height: '100%' }}
className="pane pane-light relative"
data={instructions}
context={{ activeIndexes, errorIndexes }}
increaseViewportBy={{ top: 150, bottom: 150 }}
components={{ Table, TableRow }}
rangeChanged={(range) => (virtuosoVisibleRange.current = range)}
itemContent={(index, instruction, context) => {
const isActive = context?.activeIndexes?.includes(index)
// this should only return the content which should be inside <tr> tag of each row
// for <table> and <tr> tag refer Table and TableRow components at bottom of this file
return (
<TableRowContent
index={index}
codeType={codeType}
isActive={isActive}
instruction={instruction}
splitInLines={splitInLines}
addVariableToolTipToSierraInstruction={
addVariableToolTipToSierraInstruction
}
/>
)
}}
/>
</div>
)
}

const Table = (props: any) => {
return <table className="w-full font-mono text-tiny" {...props} />
}

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 (
<tr
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,
})}
{...rest}
/>
)
}

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 (
<>
<td className={`pl-6 pr-1 px-2 whitespace-nowrap w-[1%]`}>
{index + 1}
</td>
<td className="py-1 px-2 max-w-40 break-words">
{isActive && codeType === CodeType.Sierra
? addVariableToolTipToSierraInstruction(instruction.name)
: splitInLines(instruction.name)}
</td>
</>
)
},
)
Loading

0 comments on commit 511b5ef

Please sign in to comment.