Skip to content

Commit

Permalink
Trace variables from the data picker (#5816)
Browse files Browse the repository at this point in the history
#5814

<img width="1279" alt="image"
src="https://github.com/concrete-utopia/utopia/assets/16385508/25c59ee7-de0f-4832-a575-8578113749bc">

## Description
This PR adds data tracing to the data picker. With data tracing,
variables that come from a hook call are shown with the appropriate
(green) tinted cartouche (see the screenshot above).

### Commit Details
- a new data tracing entry point is added (`traceDataFromVariableName`)
- the variable picker calls `traceDataFromVariableName` to pass the
appropriate value for the `source` prop for `CartoucheUI`

### Manual Tests
I hereby swear that:

- [x] I opened a hydrogen project and it loaded
- [x] I could navigate to various routes in Preview mode
  • Loading branch information
bkrmendy authored Jun 4, 2024
1 parent dcf5d6c commit a3aab0b
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ import {
type ObjectPath,
getEnclosingScopes,
} from './data-picker-utils'
import {
dataPathSuccess,
traceDataFromVariableName,
} from '../../../../core/data-tracing/data-tracing'
import type { ElementPath } from '../../../../core/shared/project-file-types'
import * as EP from '../../../../core/shared/element-path'
import { Substores, useEditorState } from '../../../editor/store/store-hook'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import { optionalMap } from '../../../../core/shared/optional-utils'

export const DataSelectorPopupBreadCrumbsTestId = 'data-selector-modal-top-bar'
Expand Down Expand Up @@ -270,6 +273,48 @@ export const DataSelectorModal = React.memo(
return { primitiveVars: primitives, folderVars: folders }
}, [focusedVariableChildren])

const metadata = useEditorState(
Substores.metadata,
(store) => store.editor.jsxMetadata,
'DataSelectorModal metadata',
)
const projectContents = useEditorState(
Substores.projectContents,
(store) => store.editor.projectContents,
'DataSelectorModal projectContents',
)

const variableSources = React.useMemo(() => {
let result: { [valuePath: string]: CartoucheUIProps['source'] } = {}
for (const variable of focusedVariableChildren) {
const container = variable.variableInfo.insertionCeiling
const trace = traceDataFromVariableName(
container,
variable.variableInfo.expression,
metadata,
projectContents,
dataPathSuccess([]),
)

switch (trace.type) {
case 'hook-result':
result[variable.valuePath.toString()] = 'external'
break
case 'literal-attribute':
result[variable.valuePath.toString()] = 'literal'
break
case 'component-prop':
case 'element-at-scope':
case 'failed':
result[variable.valuePath.toString()] = 'internal'
break
default:
assertNever(trace)
}
}
return result
}, [focusedVariableChildren, metadata, projectContents])

const setCurrentSelectedPathCurried = React.useCallback(
(path: DataPickerOption['valuePath']) => () => {
if (!isPrefixOf(navigatedToPath, path)) {
Expand Down Expand Up @@ -465,7 +510,7 @@ export const DataSelectorModal = React.memo(
<CartoucheUI
key={variable.valuePath.toString()}
tooltip={variableNameFromPath(variable)}
source={'internal'}
source={variableSources[variable.valuePath.toString()] ?? 'internal'}
datatype={childTypeToCartoucheDataType(variable.type)}
inverted={false}
selected={
Expand All @@ -492,7 +537,7 @@ export const DataSelectorModal = React.memo(
<CartoucheUI
tooltip={variableNameFromPath(variable)}
datatype={childTypeToCartoucheDataType(variable.type)}
source={'internal'}
source={variableSources[variable.valuePath.toString()] ?? 'internal'}
inverted={false}
selected={
selectedPath == null
Expand Down Expand Up @@ -521,7 +566,7 @@ export const DataSelectorModal = React.memo(
<CartoucheUI
key={child.valuePath.toString()}
tooltip={variableNameFromPath(child)}
source={'internal'}
source={variableSources[variable.valuePath.toString()] ?? 'internal'}
inverted={false}
datatype={childTypeToCartoucheDataType(child.type)}
selected={
Expand Down
82 changes: 82 additions & 0 deletions editor/src/core/data-tracing/data-tracing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
dataTracingToLiteralAttribute,
traceDataFromElement,
traceDataFromProp,
traceDataFromVariableName,
} from './data-tracing'

describe('Data Tracing', () => {
Expand Down Expand Up @@ -1174,6 +1175,87 @@ describe('Data Tracing', () => {
)
})
})

describe('Tracing data from the arbitrary js block of a component', () => {
it('can trace data from the arbitrary js block', async () => {
const editor = await renderTestEditorWithCode(
makeTestProjectCodeWithStoryboard(`
function MyComponent({ doc }) {
return <h1 data-uid='component-root'>{doc.title.value}</h1>
}
function useLoaderData() {
return { very: { deep: { title: {value: ['hello', 'world'] } }, a: [1, 2] } }
}
function App() {
const { very } = useLoaderData()
const { deep, a } = very
const deepButWithAccess = very.deep
const [helloFromDestructuredArray] = a
return <MyComponent data-uid='my-component' doc={deep} />
}
`),
'await-first-dom-report',
)

await focusOnComponentForTest(editor, EP.fromString('sb/app:my-component'))

{
const trace = traceDataFromVariableName(
EP.fromString('sb/app:my-component'),
'deep',
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
dataPathSuccess([]),
)

expect(trace).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component'),
'useLoaderData',
dataPathSuccess(['very', 'deep']),
),
)
}
{
const trace = traceDataFromVariableName(
EP.fromString('sb/app:my-component'),
'deepButWithAccess',
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
dataPathSuccess([]),
)

expect(trace).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component'),
'useLoaderData',
dataPathSuccess(['very', 'deep']),
),
)
}
{
const trace = traceDataFromVariableName(
EP.fromString('sb/app:my-component'),
'helloFromDestructuredArray',
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
dataPathSuccess([]),
)

expect(trace).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component'),
'useLoaderData',
dataPathSuccess(['very', 'a', '0', 'helloFromDestructuredArray']),
),
)
}
})
})
})

function makeTestProjectCodeWithStoryboard(codeForComponents: string): string {
Expand Down
35 changes: 32 additions & 3 deletions editor/src/core/data-tracing/data-tracing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ProjectContentTreeRoot } from '../../components/assets'
import { findUnderlyingTargetComponentImplementationFromImportInfo } from '../../components/custom-code/code-file'
import { withUnderlyingTarget } from '../../components/editor/store/editor-state'
import * as TPP from '../../components/template-property-path'
import { MetadataUtils } from '../model/element-metadata-utils'
Expand All @@ -18,16 +19,16 @@ import type {
UtopiaJSXComponent,
} from '../shared/element-template'
import {
emptyComments,
isJSXElement,
jsIdentifier,
type ElementInstanceMetadataMap,
isRegularParam,
isJSAssignmentStatement,
isJSIdentifier,
jsIdentifier,
emptyComments,
} from '../shared/element-template'
import { getJSXAttributesAtPath, jsxSimpleAttributeToValue } from '../shared/jsx-attribute-utils'
import { forceNotNull, optionalMap } from '../shared/optional-utils'
import { optionalMap } from '../shared/optional-utils'
import type { ElementPath, ElementPropertyPath, PropertyPath } from '../shared/project-file-types'
import * as PP from '../shared/property-path'
import { assertNever } from '../shared/utils'
Expand Down Expand Up @@ -406,6 +407,34 @@ export function traceDataFromElement(
}
}

export function traceDataFromVariableName(
enclosingScope: ElementPath,
variableName: string,
metadata: ElementInstanceMetadataMap,
projectContents: ProjectContentTreeRoot,
pathDrillSoFar: DataPathPositiveResult,
): DataTracingResult {
const componentHoldingElement = findContainingComponentForElementPath(
enclosingScope,
projectContents,
)

if (componentHoldingElement == null || componentHoldingElement.arbitraryJSBlock == null) {
return dataTracingFailed('Could not find containing component')
}

return walkUpInnerScopesUntilReachingComponent(
metadata,
projectContents,
enclosingScope,
enclosingScope,
enclosingScope,
componentHoldingElement,
jsIdentifier(variableName, '', null, emptyComments),
pathDrillSoFar,
)
}

function traceDataFromIdentifierOrAccess(
startFromElement: IdentifierOrAccess,
enclosingScope: ElementPath,
Expand Down

0 comments on commit a3aab0b

Please sign in to comment.