Skip to content

Commit

Permalink
feature(editor) Insert scoped map parameters. (#4826)
Browse files Browse the repository at this point in the history
- Refactored `ReparentTargetForPaste` into the component types and created some typeguards for the subtypes.
- Extracted out the checks for valid insertion from `pasteIntoParentOrGrandparent` into `canInsertIntoTarget`.
- Added `applyElementCeilingToReparentTarget` which can take a potential reparent target and ensure that the target does not get applied to an element above a ceiling target.
- Added `insertionCeiling` field to `VariableMetadata`.
- Refactored `JSXAttributeOtherJavaScriptKeepDeepEqualityCall` into an instance specifically for the named type and created a new instance which combines those two types.
- Added `valuesInScopeFromParameters` to `JSXMapExpression`, along with the necessary additions to the parser to extract those.
- Implemented `getOtherJavaScriptTypeFromExpression` and `getOtherJavaScriptType` utility functions.
  • Loading branch information
seanparsons authored Feb 2, 2024
1 parent cdd46df commit dccfcdc
Show file tree
Hide file tree
Showing 25 changed files with 973 additions and 384 deletions.
603 changes: 348 additions & 255 deletions editor/src/components/canvas/__snapshots__/ui-jsx-canvas.spec.tsx.snap

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import {
getJSXElementFromProjectContents,
withUnderlyingTarget,
} from '../../../editor/store/editor-state'
import type { ElementPath, Imports, NodeModules } from '../../../../core/shared/project-file-types'
import {
StaticElementPath,
type ElementPath,
type Imports,
type NodeModules,
} from '../../../../core/shared/project-file-types'
import type { CanvasCommand } from '../../commands/commands'
import { reparentElement } from '../../commands/reparent-element-command'
import type {
Expand Down Expand Up @@ -35,12 +40,12 @@ import {
} from '../../../editor/store/insertion-path'
import { getUtopiaID } from '../../../../core/shared/uid-utils'
import type { IndexPosition } from '../../../../utils/utils'
import { fastForEach } from '../../../../core/shared/utils'
import { assertNever, fastForEach } from '../../../../core/shared/utils'
import { addElements } from '../../commands/add-elements-command'
import type { ElementPathTrees } from '../../../../core/shared/element-path-tree'
import { getRequiredGroupTrueUps } from '../../commands/queue-true-up-command'
import type { Either } from '../../../../core/shared/either'
import { left, right } from '../../../../core/shared/either'
import { flatMapEither, foldEither, left, right } from '../../../../core/shared/either'
import { maybeBranchConditionalCase } from '../../../../core/model/conditionals'
import type { NonEmptyArray } from '../../../../core/shared/array-utils'
import {
Expand All @@ -54,6 +59,9 @@ import { isElementRenderedBySameComponent } from './reparent-helpers/reparent-he
import type { ParsedCopyData } from '../../../../utils/clipboard'
import { getParseSuccessForFilePath } from '../../canvas-utils'
import { renameDuplicateImports } from '../../../../core/shared/import-shared-utils'
import { modify, set } from '../../../../core/shared/optics/optic-utilities'
import { fromField, fromTypeGuard } from '../../../../core/shared/optics/optic-creators'
import { Optic } from '../../../../core/shared/optics/optics'

interface GetReparentOutcomeResult {
commands: Array<CanvasCommand>
Expand Down Expand Up @@ -366,14 +374,31 @@ function rectangleSizesEqual(a: CanvasRectangle, b: CanvasRectangle): boolean {
return a.height === b.height && a.width === b.width
}

export type ReparentTargetForPaste =
| {
type: 'sibling'
siblingPath: ElementPath
siblingBounds: CanvasRectangle
parentPath: InsertionPath
}
| { type: 'parent'; parentPath: InsertionPath }
export interface SiblingReparentTargetForPaste {
type: 'sibling'
siblingPath: ElementPath
siblingBounds: CanvasRectangle
parentPath: InsertionPath
}

export interface ParentReparentTargetForPaste {
type: 'parent'
parentPath: InsertionPath
}

export type ReparentTargetForPaste = SiblingReparentTargetForPaste | ParentReparentTargetForPaste

export function isSiblingReparentTargetForPaste(
target: ReparentTargetForPaste,
): target is SiblingReparentTargetForPaste {
return target.type === 'sibling'
}

export function isParentReparentTargetForPaste(
target: ReparentTargetForPaste,
): target is ParentReparentTargetForPaste {
return target.type === 'parent'
}

type PasteParentNotFoundError =
| 'Cannot find a suitable parent'
Expand Down Expand Up @@ -509,34 +534,45 @@ function pasteNextToSameSizedElement(
return null
}

function pasteIntoParentOrGrandparent(
elementsToInsert: JSXElementChild[],
function canInsertIntoTarget(
projectContents: ProjectContentTreeRoot,
selectedViews: NonEmptyArray<ElementPath>,
metadata: ElementInstanceMetadataMap,
elementPathTree: ElementPathTrees,
): ReparentTargetForPaste | null {
parentTarget: ElementPath,
elementsToInsert: JSXElementChild[],
): boolean {
const pastedElementNames = mapDropNulls(
(element) => (element.type === 'JSX_ELEMENT' ? element.name : null),
elementsToInsert,
)

const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true)

// paste into parent
const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText(
parentTarget,
metadata,
pastedElementNames,
)
const supportsChildren = MetadataUtils.targetSupportsChildren(
projectContents,
metadata,
parentTarget,
elementPathTree,
)

return targetElementSupportsInsertedElement && supportsChildren
}

function pasteIntoParentOrGrandparent(
elementsToInsert: JSXElementChild[],
projectContents: ProjectContentTreeRoot,
selectedViews: NonEmptyArray<ElementPath>,
metadata: ElementInstanceMetadataMap,
elementPathTree: ElementPathTrees,
): ReparentTargetForPaste | null {
const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true)

if (
MetadataUtils.targetSupportsChildren(
projectContents,
metadata,
parentTarget,
elementPathTree,
) &&
targetElementSupportsInsertedElement
canInsertIntoTarget(projectContents, metadata, elementPathTree, parentTarget, elementsToInsert)
) {
return { type: 'parent', parentPath: childInsertionPath(parentTarget) }
}
Expand All @@ -556,13 +592,70 @@ function pasteIntoParentOrGrandparent(
return null
}

const intendedPathOptic = fromTypeGuard(isParentReparentTargetForPaste)
.compose(fromField('parentPath'))
.compose(fromField('intendedParentPath'))

export function applyElementCeilingToReparentTarget(
projectContents: ProjectContentTreeRoot,
metadata: ElementInstanceMetadataMap,
elementsToInsert: JSXElementChild[],
elementPathTree: ElementPathTrees,
reparentTarget: Either<PasteParentNotFoundError, ReparentTargetForPaste>,
elementCeiling: ElementPath | null,
): Either<PasteParentNotFoundError, ReparentTargetForPaste> {
if (elementCeiling == null) {
return reparentTarget
} else {
return flatMapEither((targetForPaste) => {
switch (targetForPaste.type) {
case 'sibling':
return left('Cannot find a suitable parent')
case 'parent':
switch (targetForPaste.parentPath.type) {
case 'CHILD_INSERTION':
const intendedParentPath = targetForPaste.parentPath.intendedParentPath
// If the intended parent path is above the ceiling path then
// change it to the ceiling path instead.
const ceilingStaticPath = EP.dynamicPathToStaticPath(elementCeiling)
if (EP.depth(intendedParentPath) < EP.depth(ceilingStaticPath)) {
// Make sure it's valid to insert into.
if (
canInsertIntoTarget(
projectContents,
metadata,
elementPathTree,
ceilingStaticPath,
elementsToInsert,
)
) {
return right(set(intendedPathOptic, ceilingStaticPath, targetForPaste))
} else {
return left('Cannot find a suitable parent')
}
} else {
return right(targetForPaste)
}
case 'CONDITIONAL_CLAUSE_INSERTION':
return left('Cannot find a suitable parent')
default:
return assertNever(targetForPaste.parentPath)
}
default:
assertNever(targetForPaste)
}
}, reparentTarget)
}
}

export function getTargetParentForOneShotInsertion(
storyboardPath: ElementPath,
projectContents: ProjectContentTreeRoot,
selectedViews: Array<ElementPath>,
metadata: ElementInstanceMetadataMap,
elementsToInsert: JSXElementChild[],
elementPathTree: ElementPathTrees,
insertionCeiling: ElementPath | null,
): Either<PasteParentNotFoundError, ReparentTargetForPaste> {
if (!isNonEmptyArray(selectedViews)) {
return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) })
Expand Down Expand Up @@ -591,7 +684,14 @@ export function getTargetParentForOneShotInsertion(
elementPathTree,
)
if (pasteIntoParentOrGrandparentResult != null) {
return right(pasteIntoParentOrGrandparentResult)
return applyElementCeilingToReparentTarget(
projectContents,
metadata,
elementsToInsert,
elementPathTree,
right(pasteIntoParentOrGrandparentResult),
insertionCeiling,
)
}
return left('Cannot find a suitable parent')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
isJSXElement,
} from '../../../core/shared/element-template'
import { optionalMap } from '../../../core/shared/optional-utils'
import type { DomWalkerInvalidatePathsCtxData, UiJsxCanvasContextData } from '../ui-jsx-canvas'
import type {
DomWalkerInvalidatePathsCtxData,
UiJsxCanvasContextData,
VariableData,
} from '../ui-jsx-canvas'
import {
DomWalkerInvalidatePathsCtxAtom,
UiJsxCanvasCtxAtom,
Expand Down Expand Up @@ -207,6 +211,8 @@ export function createComponentRendererComponent(params: {
highlightBounds,
rerenderUtopiaContext.editedText,
null,
{},
[],
)

scope[JSX_CANVAS_LOOKUP_FUNCTION_NAME] = utopiaCanvasJSXLookup(
Expand All @@ -229,9 +235,10 @@ export function createComponentRendererComponent(params: {
instancePath,
)

const spiedVariablesInScope = objectMap(
const spiedVariablesInScope: VariableData = objectMap(
(spiedValue) => ({
spiedValue: spiedValue,
insertionCeiling: null,
}),
definedWithinWithValues,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export function createLookupRender(
highlightBounds: HighlightBoundsForUids | null,
editedText: ElementPath | null,
renderLimit: number | null,
variablesInScope: VariableData,
valuesInScopeFromParameters: Array<string>,
): (element: JSXElement, scope: MapLike<any>) => React.ReactChild | null {
let index = 0

Expand All @@ -117,6 +119,17 @@ export function createLookupRender(
props: attrs,
}
})

let innerVariablesInScope: VariableData = {
...variablesInScope,
}
for (const valueInScope of valuesInScopeFromParameters) {
innerVariablesInScope[valueInScope] = {
spiedValue: scope[valueInScope],
insertionCeiling: innerPath,
}
}

return renderCoreElement(
augmentedInnerElement,
innerPath,
Expand All @@ -140,7 +153,7 @@ export function createLookupRender(
code,
highlightBounds,
editedText,
{},
innerVariablesInScope,
)
}
}
Expand Down Expand Up @@ -219,6 +232,8 @@ export function renderCoreElement(
highlightBounds,
editedText,
null,
variablesInScope,
[],
)
: NoOpLookupRender

Expand Down Expand Up @@ -292,6 +307,9 @@ export function renderCoreElement(
)
}

const valuesInScopeFromParameters =
element.type === 'JSX_MAP_EXPRESSION' ? element.valuesInScopeFromParameters : []

if (elementIsTextEdited) {
const runJSExpressionLazy = () => {
const innerRender = createLookupRender(
Expand All @@ -314,6 +332,8 @@ export function renderCoreElement(
highlightBounds,
editedText,
mapCountOverride,
variablesInScope,
valuesInScopeFromParameters,
)

const blockScope = {
Expand Down Expand Up @@ -376,6 +396,8 @@ export function renderCoreElement(
highlightBounds,
editedText,
mapCountOverride,
variablesInScope,
valuesInScopeFromParameters,
)

const blockScope = {
Expand Down Expand Up @@ -864,6 +886,8 @@ function renderJSXElement(
highlightBounds,
editedText,
null,
variablesInScope,
[],
)
const blockScope = {
...inScope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export function createExecutionScope(
highlightBounds,
editedText,
null,
{},
[],
)

executionScope[JSX_CANVAS_LOOKUP_FUNCTION_NAME] = utopiaCanvasJSXLookup(
Expand Down
1 change: 1 addition & 0 deletions editor/src/components/canvas/ui-jsx-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const ElementsToRerenderGLOBAL: { current: ElementsToRerender } = {

export interface VariableMetadata {
spiedValue: unknown
insertionCeiling: ElementPath | null
}

export interface VariableData {
Expand Down
1 change: 1 addition & 0 deletions editor/src/components/editor/actions/actions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ describe('INSERT_INSERTABLE', () => {
'Spring',
[],
null,
null,
)

const targetPath = EP.elementPath([
Expand Down
1 change: 1 addition & 0 deletions editor/src/components/editor/canvas-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export const CanvasToolbar = React.memo(() => {
defaultSize: null,
source: insertableComponentGroupFragment(),
key: fragmentComponentInfo.insertMenuLabel,
insertionCeiling: null,
},
}
convertToAndClose(convertToFragmentMenuItem)
Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/editor/convert-callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function changeConditionalOrFragment(
actionsToDispatch = [
insertInsertable(
insertionPath,
insertableComponent(importsToAdd, () => element, '', [], null),
insertableComponent(importsToAdd, () => element, '', [], null, null),
fixedSizeForInsertion ? 'add-size' : 'do-not-add',
floatingMenuState.indexPosition,
),
Expand Down
1 change: 1 addition & 0 deletions editor/src/components/editor/insert-callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void
jsxMetadataRef.current,
[element.element],
elementPathTreeRef.current,
elementToInsert.value.insertionCeiling,
)

if (isLeft(targetParent)) {
Expand Down
Loading

0 comments on commit dccfcdc

Please sign in to comment.