import { Diagnostic } from '@codemirror/lint' import { useMachine, useSelector } from '@xstate/react' import { ContextMenu, ContextMenuItem } from 'components/ContextMenu' import { CustomIcon, CustomIconName } from 'components/CustomIcon' import Loading from 'components/Loading' import { useModelingContext } from 'hooks/useModelingContext' import { useKclContext } from 'lang/KclProvider' import { codeRefFromRange, getArtifactFromRange } from 'lang/std/artifactGraph' import { sourceRangeFromRust } from 'lang/wasm' import { filterOperations, getOperationIcon, getOperationLabel, stdLibMap, } from 'lib/operations' import { editorManager, engineCommandManager, kclManager } from 'lib/singletons' import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react' import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Actor, Prop } from 'xstate' import { featureTreeMachine } from 'machines/featureTreeMachine' import { editorIsMountedSelector, kclEditorActor, selectionEventSelector, } from 'machines/kclEditorMachine' export const FeatureTreePane = () => { const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector) const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector) const { send: modelingSend, state: modelingState } = useModelingContext() // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_featureTreeState, featureTreeSend] = useMachine( featureTreeMachine.provide({ guards: { codePaneIsOpen: () => modelingState.context.store.openPanes.includes('code') && editorManager.editorView !== null, }, actions: { openCodePane: () => { modelingSend({ type: 'Set context', data: { openPanes: [...modelingState.context.store.openPanes, 'code'], }, }) }, sendEditFlowStart: () => { modelingSend({ type: 'Enter sketch' }) }, scrollToError: () => { editorManager.scrollToFirstErrorDiagnosticIfExists() }, sendSelectionEvent: ({ context }) => { if (!context.targetSourceRange) { return } const artifact = context.targetSourceRange ? getArtifactFromRange( context.targetSourceRange, engineCommandManager.artifactGraph ) : null if (!artifact) { modelingSend({ type: 'Set selection', data: { selectionType: 'singleCodeCursor', selection: { codeRef: codeRefFromRange( context.targetSourceRange, kclManager.ast ), }, scrollIntoView: true, }, }) } else { modelingSend({ type: 'Set selection', data: { selectionType: 'singleCodeCursor', selection: { artifact: artifact, codeRef: codeRefFromRange( context.targetSourceRange, kclManager.ast ), }, scrollIntoView: true, }, }) } }, }, }) ) // If there are parse errors we show the last successful operations // and overlay a message on top of the pane const parseErrors = kclManager.errors.filter((e) => e.kind !== 'engine') // If there are engine errors we show the successful operations // Errors return an operation list, so use the longest one if there are multiple const longestErrorOperationList = kclManager.errors.reduce((acc, error) => { return error.operations && error.operations.length > acc.length ? error.operations : acc }, [] as Operation[]) const unfilteredOperationList = !parseErrors.length ? !kclManager.errors.length ? kclManager.execState.operations : longestErrorOperationList : kclManager.lastSuccessfulOperations // We filter out operations that are not useful to show in the feature tree const operationList = filterOperations(unfilteredOperationList) // Watch for changes in the open panes and send an event to the feature tree machine useEffect(() => { const codeOpen = modelingState.context.store.openPanes.includes('code') if (codeOpen && isEditorMounted) { featureTreeSend({ type: 'codePaneOpened' }) } }, [modelingState.context.store.openPanes, isEditorMounted]) // Watch for changes in the selection and send an event to the feature tree machine useEffect(() => { featureTreeSend({ type: 'selected' }) }, [lastSelectionEvent]) function goToError() { featureTreeSend({ type: 'goToError' }) } return (
{kclManager.isExecuting ? ( Building feature tree... ) : ( <> {parseErrors.length > 0 && (

Errors found in KCL code.
Please fix them before continuing.

)} {operationList.map((operation) => { const key = `${operation.type}-${ 'name' in operation ? operation.name : 'anonymous' }-${ 'sourceRange' in operation ? operation.sourceRange[0] : 'start' }` return ( ) })} )}
) } export const visibilityMap = new Map() interface VisibilityToggleProps { entityId: string initialVisibility: boolean onVisibilityChange?: () => void } /** * A button that toggles the visibility of an entity * tied to an artifact in the feature tree. * TODO: this is unimplemented and will be used for * default planes after we fix them and add them to the artifact graph / feature tree */ const VisibilityToggle = (props: VisibilityToggleProps) => { const [visible, setVisible] = useState(props.initialVisibility) function handleToggleVisible() { setVisible(!visible) visibilityMap.set(props.entityId, !visible) props.onVisibilityChange?.() } return ( ) } /** * More generic version of OperationListItem, * to be used for default planes after we fix them and * add them to the artifact graph / feature tree */ const OperationItemWrapper = ({ icon, name, visibilityToggle, menuItems, errors, className, ...props }: React.HTMLAttributes & { icon: CustomIconName name: string visibilityToggle?: VisibilityToggleProps menuItems?: ComponentProps['items'] errors?: Diagnostic[] }) => { const menuRef = useRef(null) return (
{errors && errors.length > 0 && ( has error )} {visibilityToggle && } {menuItems && ( )}
) } /** * A button with an icon, name, and context menu * for an operation in the feature tree. */ const OperationItem = (props: { item: Operation send: Prop, 'send'> }) => { const kclContext = useKclContext() const name = 'name' in props.item && props.item.name !== null ? getOperationLabel(props.item) : 'anonymous' const errors = useMemo(() => { return kclContext.diagnostics.filter( (diag) => diag.severity === 'error' && 'sourceRange' in props.item && diag.from >= props.item.sourceRange[0] && diag.to <= props.item.sourceRange[1] ) }, [kclContext.diagnostics.length]) function selectOperation() { if (props.item.type === 'UserDefinedFunctionReturn') { return } props.send({ type: 'selectOperation', data: { targetSourceRange: sourceRangeFromRust(props.item.sourceRange), }, }) } /** * For now we can only enter the "edit" flow for the startSketchOn operation. * TODO: https://github.com/KittyCAD/modeling-app/issues/4442 */ function enterEditFlow() { if (props.item.type === 'StdLibCall') { props.send({ type: 'enterEditFlow', data: { targetSourceRange: sourceRangeFromRust(props.item.sourceRange), currentOperation: props.item, }, }) } } const menuItems = useMemo( () => [ { if (props.item.type === 'UserDefinedFunctionReturn') { return } props.send({ type: 'goToKclSource', data: { targetSourceRange: sourceRangeFromRust(props.item.sourceRange), }, }) }} > View KCL source code , ...(props.item.type === 'UserDefinedFunctionCall' ? [ { if (props.item.type !== 'UserDefinedFunctionCall') { return } const functionRange = props.item.functionSourceRange // For some reason, the cursor goes to the end of the source // range we select. So set the end equal to the beginning. functionRange[1] = functionRange[0] props.send({ type: 'goToKclSource', data: { targetSourceRange: sourceRangeFromRust(functionRange), }, }) }} > View function definition , ] : []), ...(props.item.type === 'StdLibCall' && stdLibMap[props.item.name]?.prepareToEdit ? [ Edit {name} , ] : []), ], [props.item, props.send] ) return ( ) }