From a3aab0b361d7cb536b0c1d7304406a56b271738d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertalan=20K=C3=B6rmendy?= Date: Tue, 4 Jun 2024 11:41:34 +0200 Subject: [PATCH] Trace variables from the data picker (#5816) https://github.com/concrete-utopia/utopia/issues/5814 image ## 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 --- .../component-section/data-selector-modal.tsx | 53 +++++++++++- .../core/data-tracing/data-tracing.spec.ts | 82 +++++++++++++++++++ editor/src/core/data-tracing/data-tracing.ts | 35 +++++++- 3 files changed, 163 insertions(+), 7 deletions(-) diff --git a/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx b/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx index 70d310c3aed1..dd4888e9fcaa 100644 --- a/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx +++ b/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx @@ -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' @@ -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)) { @@ -465,7 +510,7 @@ export const DataSelectorModal = React.memo( { @@ -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

{doc.title.value}

+ } + + 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 + } + `), + '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 { diff --git a/editor/src/core/data-tracing/data-tracing.ts b/editor/src/core/data-tracing/data-tracing.ts index 5bd44f51a24c..cdd420c54886 100644 --- a/editor/src/core/data-tracing/data-tracing.ts +++ b/editor/src/core/data-tracing/data-tracing.ts @@ -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' @@ -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' @@ -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,