Add grouping of module instances in Feature Tree (#6125)

* Rename operations to be more generic grouping

* Add group enum

* Add module instance groups

* Change to export all operation ts-rs types to the same file

* Fix Feature Tree display of modules to use name

* Ignore clippy warning

* Update output after operation changes

* Change module instances in Feature Tree use to import icon

* Fix error message when attempting to delete module instance
This commit is contained in:
Jonathan Tran
2025-04-03 22:10:39 -04:00
committed by GitHub
parent c7b348390e
commit d38dcb9ba2
65 changed files with 12299 additions and 9315 deletions

View File

@ -289,10 +289,7 @@ const OperationItem = (props: {
send: Prop<Actor<typeof featureTreeMachine>, 'send'>
}) => {
const kclContext = useKclContext()
const name =
'name' in props.item && props.item.name !== null
? getOperationLabel(props.item)
: 'anonymous'
const name = getOperationLabel(props.item)
const errors = useMemo(() => {
return kclContext.diagnostics.filter(
(diag) =>
@ -304,7 +301,7 @@ const OperationItem = (props: {
}, [kclContext.diagnostics.length])
function selectOperation() {
if (props.item.type === 'UserDefinedFunctionReturn') {
if (props.item.type === 'GroupEnd') {
return
}
props.send({
@ -352,7 +349,7 @@ const OperationItem = (props: {
function deleteOperation() {
if (
props.item.type === 'StdLibCall' ||
props.item.type === 'UserDefinedFunctionCall' ||
props.item.type === 'GroupBegin' ||
props.item.type === 'KclStdLibCall'
) {
props.send({
@ -368,7 +365,7 @@ const OperationItem = (props: {
() => [
<ContextMenuItem
onClick={() => {
if (props.item.type === 'UserDefinedFunctionReturn') {
if (props.item.type === 'GroupEnd') {
return
}
props.send({
@ -381,14 +378,19 @@ const OperationItem = (props: {
>
View KCL source code
</ContextMenuItem>,
...(props.item.type === 'UserDefinedFunctionCall'
...(props.item.type === 'GroupBegin' &&
props.item.group.type === 'FunctionCall'
? [
<ContextMenuItem
onClick={() => {
if (props.item.type !== 'UserDefinedFunctionCall') {
if (props.item.type !== 'GroupBegin') {
return
}
const functionRange = props.item.functionSourceRange
if (props.item.group.type !== 'FunctionCall') {
// TODO: Add module instance support.
return
}
const functionRange = props.item.group.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]

View File

@ -1303,7 +1303,9 @@ export async function deleteFromSelection(
if (!pathToNode) return new Error('Could not find extrude variable')
} else {
pathToNode = selection.codeRef.pathToNode
if (varDec.node.type !== 'VariableDeclarator') {
if (varDec.node.type === 'VariableDeclarator') {
extrudeNameToDelete = varDec.node.id.name
} else if (varDec.node.type === 'CallExpression') {
const callExp = getNodeFromPath<CallExpression>(
astClone,
pathToNode,
@ -1312,7 +1314,7 @@ export async function deleteFromSelection(
if (err(callExp)) return callExp
extrudeNameToDelete = callExp.node.callee.name.name
} else {
extrudeNameToDelete = varDec.node.id.name
return new Error('Could not find extrude variable or call')
}
}

View File

@ -16,17 +16,20 @@ function stdlib(name: string): Operation {
function userCall(name: string): Operation {
return {
type: 'UserDefinedFunctionCall',
name,
functionSourceRange: defaultSourceRange(),
unlabeledArg: null,
labeledArgs: {},
type: 'GroupBegin',
group: {
type: 'FunctionCall',
name,
functionSourceRange: defaultSourceRange(),
unlabeledArg: null,
labeledArgs: {},
},
sourceRange: defaultSourceRange(),
}
}
function userReturn(): Operation {
return {
type: 'UserDefinedFunctionReturn',
type: 'GroupEnd',
}
}

View File

@ -1,5 +1,4 @@
import type { Operation } from '@rust/kcl-lib/bindings/Operation'
import type { OpKclValue } from '@rust/kcl-lib/bindings/OpKclValue'
import type { Operation, OpKclValue } from '@rust/kcl-lib/bindings/Operation'
import type { CustomIconName } from '@src/components/CustomIcon'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
@ -1118,10 +1117,20 @@ export function getOperationLabel(op: Operation): string {
return stdLibMap[op.name]?.label ?? op.name
case 'KclStdLibCall':
return stdLibMap[op.name]?.label ?? op.name
case 'UserDefinedFunctionCall':
return op.name ?? 'Anonymous custom function'
case 'UserDefinedFunctionReturn':
return 'User function return'
case 'GroupBegin':
if (op.group.type === 'FunctionCall') {
return op.group.name ?? 'anonymous'
} else if (op.group.type === 'ModuleInstance') {
return op.group.name
} else {
const _exhaustiveCheck: never = op.group
return '' // unreachable
}
case 'GroupEnd':
return 'Group end'
default:
const _exhaustiveCheck: never = op
return '' // unreachable
}
}
@ -1134,8 +1143,16 @@ export function getOperationIcon(op: Operation): CustomIconName {
return stdLibMap[op.name]?.icon ?? 'questionMark'
case 'KclStdLibCall':
return stdLibMap[op.name]?.icon ?? 'questionMark'
default:
case 'GroupBegin':
if (op.group.type === 'ModuleInstance') {
return 'import' // TODO: Use insert icon.
}
return 'make-variable'
case 'GroupEnd':
return 'questionMark'
default:
const _exhaustiveCheck: never = op
return 'questionMark' // unreachable
}
}
@ -1151,31 +1168,31 @@ export function filterOperations(operations: Operation[]): Operation[] {
* for use in the feature tree UI
*/
const operationFilters = [
isNotUserFunctionWithNoOperations,
isNotInsideUserFunction,
isNotUserFunctionReturn,
isNotGroupWithNoOperations,
isNotInsideGroup,
isNotGroupEnd,
]
/**
* A filter to exclude everything that occurs inside a UserDefinedFunctionCall
* and its corresponding UserDefinedFunctionReturn from a list of operations.
* This works even when there are nested function calls.
* A filter to exclude everything that occurs inside a GroupBegin and its
* corresponding GroupEnd from a list of operations. This works even when there
* are nested function calls and module instances.
*/
function isNotInsideUserFunction(operations: Operation[]): Operation[] {
function isNotInsideGroup(operations: Operation[]): Operation[] {
const ops: Operation[] = []
let depth = 0
for (const op of operations) {
if (depth === 0) {
ops.push(op)
}
if (op.type === 'UserDefinedFunctionCall') {
if (op.type === 'GroupBegin') {
depth++
}
if (op.type === 'UserDefinedFunctionReturn') {
if (op.type === 'GroupEnd') {
depth--
console.assert(
depth >= 0,
'Unbalanced UserDefinedFunctionCall and UserDefinedFunctionReturn; too many returns'
'Unbalanced GroupBegin and GroupEnd; too many ends'
)
}
}
@ -1184,26 +1201,23 @@ function isNotInsideUserFunction(operations: Operation[]): Operation[] {
}
/**
* A filter to exclude UserDefinedFunctionCall operations and their
* corresponding UserDefinedFunctionReturn that don't have any operations inside
* them from a list of operations.
* A filter to exclude GroupBegin operations and their corresponding GroupEnd
* that don't have any operations inside them from a list of operations.
*/
function isNotUserFunctionWithNoOperations(
operations: Operation[]
): Operation[] {
function isNotGroupWithNoOperations(operations: Operation[]): Operation[] {
return operations.filter((op, index) => {
if (
op.type === 'UserDefinedFunctionCall' &&
// If this is a call at the end of the array, it's preserved.
op.type === 'GroupBegin' &&
// If this is a begin at the end of the array, it's preserved.
index < operations.length - 1 &&
operations[index + 1].type === 'UserDefinedFunctionReturn'
operations[index + 1].type === 'GroupEnd'
)
return false
if (
op.type === 'UserDefinedFunctionReturn' &&
// If this return is at the beginning of the array, it's preserved.
op.type === 'GroupEnd' &&
// If this is an end at the beginning of the array, it's preserved.
index > 0 &&
operations[index - 1].type === 'UserDefinedFunctionCall'
operations[index - 1].type === 'GroupBegin'
)
return false
@ -1212,11 +1226,10 @@ function isNotUserFunctionWithNoOperations(
}
/**
* A filter to exclude UserDefinedFunctionReturn operations from a list of
* operations.
* A filter to exclude GroupEnd operations from a list of operations.
*/
function isNotUserFunctionReturn(ops: Operation[]): Operation[] {
return ops.filter((op) => op.type !== 'UserDefinedFunctionReturn')
function isNotGroupEnd(ops: Operation[]): Operation[] {
return ops.filter((op) => op.type !== 'GroupEnd')
}
export interface EnterEditFlowProps {
@ -1230,7 +1243,7 @@ export async function enterEditFlow({
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
return new Error(
'Feature tree editing not yet supported for user-defined functions. Please edit in the code editor.'
'Feature tree editing not yet supported for user-defined functions or modules. Please edit in the code editor.'
)
}
const stdLibInfo = stdLibMap[operation.name]
@ -1269,7 +1282,7 @@ export async function enterAppearanceFlow({
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
return new Error(
'Appearance setting not yet supported for user-defined functions. Please edit in the code editor.'
'Appearance setting not yet supported for user-defined functions or modules. Please edit in the code editor.'
)
}
const stdLibInfo = stdLibMap[operation.name]