Add edit flow for named constants / parameters (#5911)
* Add support for forcing kcl input create variable * Command palette padding tweak * Make traverse function work for ExpressionStatements * Add utilities for getting earliest safe index in AST * Fix the insertIndex logic to not be based on the selection anymore * Add workflow to create a named constant * Fix bug with nameEndInDigits matcher * Tweak command config * Add a three-dot menu to feature tree pane to create parameters * Add E2E test for create parameter flow * Remove edit flow oops * Fix tsc error * Fix E2E test * Update named constant position in edit flow test * Add tags into consideration for safe insert index Per @Irev-dev's helpful feedback, with unit tests! * Fix tsc by removing a generic type * Remove unused imports * Fix lints * A snapshot a day keeps the bugs away! 📷🐛 * Add utilities for working with variable declarations * Add "edit parameter" user flow * Add edit flow config * WIP working on de-bloating useCalculateKclExpreesion * Add the ability to specify a `displayName` for an arg * Add utility to type check on SourceRanges * Review step design tweak fixes * Refactor useCalculateKclExpression to take a sourceRange * Make option arg validation work for objects and arrays Using an admittedly dumb stringification approach * Make edit flow never move the constant to be edited * Add E2E test section * Fix lints * Remove lying comment, tiny CSS tweak * A snapshot a day keeps the bugs away! 📷🐛 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -489,7 +489,7 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test(`Can add a named parameter or constant`, async ({
|
test(`Can add and edit a named parameter or constant`, async ({
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
context,
|
context,
|
||||||
@ -511,7 +511,7 @@ c = 3 + a`
|
|||||||
// but you do because all modeling commands have that requirement
|
// but you do because all modeling commands have that requirement
|
||||||
await scene.settled(cmdBar)
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
await test.step(`Go through the command palette flow`, async () => {
|
await test.step(`Create a parameter via command bar`, async () => {
|
||||||
await cmdBar.cmdBarOpenBtn.click()
|
await cmdBar.cmdBarOpenBtn.click()
|
||||||
await cmdBar.chooseCommand('create parameter')
|
await cmdBar.chooseCommand('create parameter')
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
@ -536,5 +536,57 @@ c = 3 + a`
|
|||||||
await editor.expectEditor.toContain(
|
await editor.expectEditor.toContain(
|
||||||
`a = 5b = a * amyParameter001 = b - 5c = 3 + a`
|
`a = 5b = a * amyParameter001 = b - 5c = 3 + a`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const newValue = `2 * b + a`
|
||||||
|
await test.step(`Edit the parameter via command bar`, async () => {
|
||||||
|
await cmdBar.cmdBarOpenBtn.click()
|
||||||
|
await cmdBar.chooseCommand('edit parameter')
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Edit parameter',
|
||||||
|
currentArgKey: 'Name',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Name: '',
|
||||||
|
Value: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Name',
|
||||||
|
})
|
||||||
|
await cmdBar
|
||||||
|
.selectOption({
|
||||||
|
name: 'myParameter001',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Edit parameter',
|
||||||
|
currentArgKey: 'value',
|
||||||
|
currentArgValue: 'b - 5',
|
||||||
|
headerArguments: {
|
||||||
|
Name: 'myParameter001',
|
||||||
|
Value: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'value',
|
||||||
|
})
|
||||||
|
await cmdBar.argumentInput.locator('[contenteditable]').fill(newValue)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
commandName: 'Edit parameter',
|
||||||
|
headerArguments: {
|
||||||
|
Name: 'myParameter001',
|
||||||
|
// KCL inputs show the *computed* value, not the input value, in the command palette header
|
||||||
|
Value: '55',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'commandBarClosed',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`a = 5b = a * amyParameter001 = ${newValue}c = 3 + a`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
@ -54,7 +54,7 @@ function ArgumentInput({
|
|||||||
return (
|
return (
|
||||||
<CommandArgOptionInput
|
<CommandArgOptionInput
|
||||||
arg={arg}
|
arg={arg}
|
||||||
argName={arg.name}
|
argName={arg.displayName || arg.name}
|
||||||
stepBack={stepBack}
|
stepBack={stepBack}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
placeholder="Select an option"
|
placeholder="Select an option"
|
||||||
@ -71,7 +71,7 @@ function ArgumentInput({
|
|||||||
{ name: 'Off', value: false },
|
{ name: 'Off', value: false },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
argName={arg.name}
|
argName={arg.displayName || arg.name}
|
||||||
stepBack={stepBack}
|
stepBack={stepBack}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
placeholder="Select an option"
|
placeholder="Select an option"
|
||||||
|
@ -38,7 +38,7 @@ function CommandBarBasicInput({
|
|||||||
className="flex items-center mx-4 my-4"
|
className="flex items-center mx-4 my-4"
|
||||||
>
|
>
|
||||||
<span className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
|
<span className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
|
||||||
{arg.name}
|
{arg.displayName || arg.name}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
data-testid="cmd-bar-arg-value"
|
data-testid="cmd-bar-arg-value"
|
||||||
|
@ -130,7 +130,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
data-test-name="arg-name"
|
data-test-name="arg-name"
|
||||||
className="capitalize"
|
className="capitalize"
|
||||||
>
|
>
|
||||||
{argName}
|
{arg.displayName || argName}
|
||||||
</span>
|
</span>
|
||||||
<span className="sr-only">: </span>
|
<span className="sr-only">: </span>
|
||||||
<span data-testid="header-arg-value">
|
<span data-testid="header-arg-value">
|
||||||
|
@ -21,10 +21,16 @@ import { useSelector } from '@xstate/react'
|
|||||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { useSettings } from 'machines/appMachine'
|
import { useSettings } from 'machines/appMachine'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import { AnyStateMachine, SnapshotFrom } from 'xstate'
|
||||||
|
import { kclManager } from 'lib/singletons'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { isPathToNode, SourceRange, VariableDeclarator } from 'lang/wasm'
|
||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
|
|
||||||
const machineContextSelector = (snapshot?: {
|
// TODO: remove the need for this selector once we decouple all actors from React
|
||||||
context: Record<string, unknown>
|
const machineContextSelector = (snapshot?: SnapshotFrom<AnyStateMachine>) =>
|
||||||
}) => snapshot?.context
|
snapshot?.context
|
||||||
|
|
||||||
function CommandBarKclInput({
|
function CommandBarKclInput({
|
||||||
arg,
|
arg,
|
||||||
@ -47,6 +53,16 @@ function CommandBarKclInput({
|
|||||||
arg.machineActor,
|
arg.machineActor,
|
||||||
machineContextSelector
|
machineContextSelector
|
||||||
)
|
)
|
||||||
|
const sourceRangeForPrevVariables = useMemo<SourceRange | undefined>(() => {
|
||||||
|
const nodeToEdit = commandBarState.context.argumentsToSubmit.nodeToEdit
|
||||||
|
const pathToNode = isPathToNode(nodeToEdit) ? nodeToEdit : undefined
|
||||||
|
const node = pathToNode
|
||||||
|
? getNodeFromPath<Node<VariableDeclarator>>(kclManager.ast, pathToNode)
|
||||||
|
: undefined
|
||||||
|
return !err(node) && node && node.node.type === 'VariableDeclarator'
|
||||||
|
? [node.node.start, node.node.end, node.node.moduleId]
|
||||||
|
: undefined
|
||||||
|
}, [kclManager.ast, commandBarState.context.argumentsToSubmit.nodeToEdit])
|
||||||
const defaultValue = useMemo(
|
const defaultValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
arg.defaultValue
|
arg.defaultValue
|
||||||
@ -90,21 +106,22 @@ function CommandBarKclInput({
|
|||||||
const editorRef = useRef<HTMLDivElement>(null)
|
const editorRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
prevVariables,
|
|
||||||
calcResult,
|
calcResult,
|
||||||
newVariableInsertIndex,
|
newVariableInsertIndex,
|
||||||
valueNode,
|
valueNode,
|
||||||
newVariableName,
|
newVariableName,
|
||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
|
prevVariables,
|
||||||
} = useCalculateKclExpression({
|
} = useCalculateKclExpression({
|
||||||
value,
|
value,
|
||||||
initialVariableName,
|
initialVariableName,
|
||||||
|
sourceRange: sourceRangeForPrevVariables,
|
||||||
})
|
})
|
||||||
|
|
||||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||||
label: v.key,
|
label: v.key,
|
||||||
detail: String(roundOff(v.value as number)),
|
detail: String(roundOff(Number(v.value))),
|
||||||
}))
|
}))
|
||||||
const varMentionsExtension = varMentions(varMentionData)
|
const varMentionsExtension = varMentions(varMentionData)
|
||||||
|
|
||||||
@ -219,13 +236,18 @@ function CommandBarKclInput({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id="arg-form" onSubmit={handleSubmit} data-can-submit={canSubmit}>
|
<form
|
||||||
|
id="arg-form"
|
||||||
|
className="mb-2"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
data-can-submit={canSubmit}
|
||||||
|
>
|
||||||
<label className="flex gap-4 items-center mx-4 my-4 border-solid border-b border-chalkboard-50">
|
<label className="flex gap-4 items-center mx-4 my-4 border-solid border-b border-chalkboard-50">
|
||||||
<span
|
<span
|
||||||
data-testid="cmd-bar-arg-name"
|
data-testid="cmd-bar-arg-name"
|
||||||
className="capitalize text-chalkboard-80 dark:text-chalkboard-20"
|
className="capitalize text-chalkboard-80 dark:text-chalkboard-20"
|
||||||
>
|
>
|
||||||
{arg.name}
|
{arg.displayName || arg.name}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
data-testid="cmd-bar-arg-value"
|
data-testid="cmd-bar-arg-value"
|
||||||
@ -249,7 +271,7 @@ function CommandBarKclInput({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{createNewVariable ? (
|
{createNewVariable ? (
|
||||||
<div className="flex mb-2 items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
|
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
|
||||||
<label
|
<label
|
||||||
htmlFor="variable-name"
|
htmlFor="variable-name"
|
||||||
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
|
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
|
||||||
|
@ -58,7 +58,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandBarHeader>
|
<CommandBarHeader>
|
||||||
<p className="px-4">
|
<p className="px-4 pb-2">
|
||||||
{selectedCommand?.reviewMessage ? (
|
{selectedCommand?.reviewMessage ? (
|
||||||
selectedCommand.reviewMessage instanceof Function ? (
|
selectedCommand.reviewMessage instanceof Function ? (
|
||||||
selectedCommand.reviewMessage(commandBarState.context)
|
selectedCommand.reviewMessage(commandBarState.context)
|
||||||
@ -66,7 +66,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
selectedCommand.reviewMessage
|
selectedCommand.reviewMessage
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>Confirm {selectedCommand?.name}</>
|
<>Confirm {selectedCommand?.displayName || selectedCommand?.name}</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form
|
||||||
|
@ -40,7 +40,7 @@ function CommandBarTextareaInput({
|
|||||||
data-testid="cmd-bar-arg-name"
|
data-testid="cmd-bar-arg-name"
|
||||||
className="capitalize px-2 py-1 rounded-br bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
className="capitalize px-2 py-1 rounded-br bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||||
>
|
>
|
||||||
{arg.name}
|
{arg.displayName || arg.name}
|
||||||
</span>
|
</span>
|
||||||
<textarea
|
<textarea
|
||||||
data-testid="cmd-bar-arg-value"
|
data-testid="cmd-bar-arg-value"
|
||||||
|
@ -21,11 +21,6 @@ export function getSafeInsertIndex(
|
|||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
const tagDeclarators = getTagDeclaratorsInProgram(program)
|
const tagDeclarators = getTagDeclaratorsInProgram(program)
|
||||||
console.log('FRANK tagDeclarators', {
|
|
||||||
identifiers,
|
|
||||||
tagDeclarators,
|
|
||||||
targetExpr,
|
|
||||||
})
|
|
||||||
const safeTagIndex = tagDeclarators.reduce((acc, curr) => {
|
const safeTagIndex = tagDeclarators.reduce((acc, curr) => {
|
||||||
return identifiers.findIndex((a) => a.name === curr.tag.value) === -1
|
return identifiers.findIndex((a) => a.name === curr.tag.value) === -1
|
||||||
? acc
|
? acc
|
||||||
|
22
src/lang/queryAst/getVariableDeclaration.ts
Normal file
22
src/lang/queryAst/getVariableDeclaration.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { Program } from 'lang/wasm'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a program and a variable name, return the variable declaration
|
||||||
|
* if it exists.
|
||||||
|
*/
|
||||||
|
export function getVariableDeclaration(
|
||||||
|
program: Node<Program>,
|
||||||
|
variableName: string
|
||||||
|
) {
|
||||||
|
const foundItem = program.body.find(
|
||||||
|
(item) =>
|
||||||
|
item.type === 'VariableDeclaration' &&
|
||||||
|
item.declaration.id.name === variableName
|
||||||
|
)
|
||||||
|
if (foundItem?.type === 'VariableDeclaration') {
|
||||||
|
return foundItem
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
17
src/lang/queryAst/getVariableDeclarationIndex.ts
Normal file
17
src/lang/queryAst/getVariableDeclarationIndex.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { Program } from 'lang/wasm'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a program and a variable name, return the index of the variable declaration
|
||||||
|
* in the body, returning `-1` if it doesn't exist.
|
||||||
|
*/
|
||||||
|
export function getVariableDeclarationIndex(
|
||||||
|
program: Node<Program>,
|
||||||
|
variableName: string
|
||||||
|
) {
|
||||||
|
return program.body.findIndex(
|
||||||
|
(item) =>
|
||||||
|
item.type === 'VariableDeclaration' &&
|
||||||
|
item.declaration.id.name === variableName
|
||||||
|
)
|
||||||
|
}
|
@ -54,6 +54,7 @@ import { UnitLen } from '@rust/kcl-lib/bindings/UnitLen'
|
|||||||
import { UnitAngle as UnitAng } from '@rust/kcl-lib/bindings/UnitAngle'
|
import { UnitAngle as UnitAng } from '@rust/kcl-lib/bindings/UnitAngle'
|
||||||
import { ModulePath } from '@rust/kcl-lib/bindings/ModulePath'
|
import { ModulePath } from '@rust/kcl-lib/bindings/ModulePath'
|
||||||
import { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes'
|
import { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes'
|
||||||
|
import { isArray } from 'lib/utils'
|
||||||
|
|
||||||
export type { Artifact } from '@rust/kcl-lib/bindings/Artifact'
|
export type { Artifact } from '@rust/kcl-lib/bindings/Artifact'
|
||||||
export type { ArtifactCommand } from '@rust/kcl-lib/bindings/Artifact'
|
export type { ArtifactCommand } from '@rust/kcl-lib/bindings/Artifact'
|
||||||
@ -285,6 +286,13 @@ export const isPathToNodeNumber = (
|
|||||||
return typeof pathToNode === 'number'
|
return typeof pathToNode === 'number'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isPathToNode = (input: unknown): input is PathToNode =>
|
||||||
|
isArray(input) &&
|
||||||
|
isArray(input[0]) &&
|
||||||
|
input[0].length == 2 &&
|
||||||
|
(typeof input[0][0] === 'number' || typeof input[0][0] === 'string') &&
|
||||||
|
typeof input[0][1] === 'string'
|
||||||
|
|
||||||
export interface ExecState {
|
export interface ExecState {
|
||||||
variables: { [key in string]?: KclValue }
|
variables: { [key in string]?: KclValue }
|
||||||
operations: Operation[]
|
operations: Operation[]
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
|
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
|
||||||
import { transformAstSketchLines } from 'lang/std/sketchcombos'
|
import { transformAstSketchLines } from 'lang/std/sketchcombos'
|
||||||
import { PathToNode } from 'lang/wasm'
|
import {
|
||||||
|
isPathToNode,
|
||||||
|
PathToNode,
|
||||||
|
SourceRange,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from 'lang/wasm'
|
||||||
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
||||||
import { components } from 'lib/machine-api'
|
import { components } from 'lib/machine-api'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
||||||
import {
|
import {
|
||||||
@ -15,6 +20,9 @@ import {
|
|||||||
shellValidator,
|
shellValidator,
|
||||||
sweepValidator,
|
sweepValidator,
|
||||||
} from './validators'
|
} from './validators'
|
||||||
|
import { getVariableDeclaration } from 'lang/queryAst/getVariableDeclaration'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
type OutputTypeKey = OutputFormat['type']
|
type OutputTypeKey = OutputFormat['type']
|
||||||
@ -95,6 +103,10 @@ export type ModelingCommandSchema = {
|
|||||||
'event.parameter.create': {
|
'event.parameter.create': {
|
||||||
value: KclCommandValue
|
value: KclCommandValue
|
||||||
}
|
}
|
||||||
|
'event.parameter.edit': {
|
||||||
|
nodeToEdit: PathToNode
|
||||||
|
value: KclCommandValue
|
||||||
|
}
|
||||||
'change tool': {
|
'change tool': {
|
||||||
tool: SketchTool
|
tool: SketchTool
|
||||||
}
|
}
|
||||||
@ -601,6 +613,77 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'event.parameter.edit': {
|
||||||
|
displayName: 'Edit parameter',
|
||||||
|
description: 'Edit the value of a named constant',
|
||||||
|
icon: 'make-variable',
|
||||||
|
status: 'development',
|
||||||
|
needsReview: false,
|
||||||
|
args: {
|
||||||
|
nodeToEdit: {
|
||||||
|
displayName: 'Name',
|
||||||
|
inputType: 'options',
|
||||||
|
valueSummary: (nodeToEdit: PathToNode) => {
|
||||||
|
const node = getNodeFromPath<VariableDeclarator>(
|
||||||
|
kclManager.ast,
|
||||||
|
nodeToEdit
|
||||||
|
)
|
||||||
|
if (err(node) || node.node.type !== 'VariableDeclarator')
|
||||||
|
return 'Error'
|
||||||
|
return node.node.id.name || ''
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
options() {
|
||||||
|
return (
|
||||||
|
Object.entries(kclManager.execState.variables)
|
||||||
|
// TODO: @franknoirot && @jtran would love to make this go away soon 🥺
|
||||||
|
.filter(([_, variable]) => variable?.type === 'Number')
|
||||||
|
.map(([name, variable]) => {
|
||||||
|
const node = getVariableDeclaration(kclManager.ast, name)
|
||||||
|
if (node === undefined) return
|
||||||
|
const range: SourceRange = [node.start, node.end, node.moduleId]
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
range
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
value: pathToNode,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((a) => !!a) || []
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: true,
|
||||||
|
defaultValue(commandBarContext) {
|
||||||
|
const nodeToEdit = commandBarContext.argumentsToSubmit.nodeToEdit
|
||||||
|
if (!nodeToEdit || !isPathToNode(nodeToEdit)) return '5'
|
||||||
|
const node = getNodeFromPath<VariableDeclarator>(
|
||||||
|
kclManager.ast,
|
||||||
|
nodeToEdit
|
||||||
|
)
|
||||||
|
if (err(node) || node.node.type !== 'VariableDeclarator')
|
||||||
|
return 'Error'
|
||||||
|
const variableName = node.node.id.name || ''
|
||||||
|
if (typeof variableName !== 'string') return '5'
|
||||||
|
const variableNode = getVariableDeclaration(
|
||||||
|
kclManager.ast,
|
||||||
|
variableName
|
||||||
|
)
|
||||||
|
if (!variableNode) return '5'
|
||||||
|
const code = codeManager.code.slice(
|
||||||
|
variableNode.declaration.init.start,
|
||||||
|
variableNode.declaration.init.end
|
||||||
|
)
|
||||||
|
return code
|
||||||
|
},
|
||||||
|
createVariable: 'disallow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'Constrain length': {
|
'Constrain length': {
|
||||||
description: 'Constrain the length of one or more segments.',
|
description: 'Constrain the length of one or more segments.',
|
||||||
icon: 'dimension',
|
icon: 'dimension',
|
||||||
|
@ -114,6 +114,7 @@ export type CommandArgumentConfig<
|
|||||||
OutputType,
|
OutputType,
|
||||||
C = ContextFrom<AnyStateMachine>
|
C = ContextFrom<AnyStateMachine>
|
||||||
> = {
|
> = {
|
||||||
|
displayName?: string
|
||||||
description?: string
|
description?: string
|
||||||
required:
|
required:
|
||||||
| boolean
|
| boolean
|
||||||
@ -236,6 +237,7 @@ export type CommandArgument<
|
|||||||
OutputType,
|
OutputType,
|
||||||
T extends AnyStateMachine = AnyStateMachine
|
T extends AnyStateMachine = AnyStateMachine
|
||||||
> = {
|
> = {
|
||||||
|
displayName?: string
|
||||||
description?: string
|
description?: string
|
||||||
required:
|
required:
|
||||||
| boolean
|
| boolean
|
||||||
|
@ -160,6 +160,7 @@ export function buildCommandArgument<
|
|||||||
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
|
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
|
||||||
// You need to manually add key/value pairs here.
|
// You need to manually add key/value pairs here.
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
|
displayName: arg.displayName,
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
hidden: arg.hidden,
|
hidden: arg.hidden,
|
||||||
|
@ -3,7 +3,7 @@ import { kclManager } from 'lib/singletons'
|
|||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { findUniqueName } from 'lang/modifyAst'
|
import { findUniqueName } from 'lang/modifyAst'
|
||||||
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
|
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
|
||||||
import { Expr } from 'lang/wasm'
|
import { Expr, SourceRange } from 'lang/wasm'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { getCalculatedKclExpressionValue } from './kclHelpers'
|
import { getCalculatedKclExpressionValue } from './kclHelpers'
|
||||||
import { parse, resultIsOk } from 'lang/wasm'
|
import { parse, resultIsOk } from 'lang/wasm'
|
||||||
@ -21,9 +21,11 @@ const isValidVariableName = (name: string) =>
|
|||||||
export function useCalculateKclExpression({
|
export function useCalculateKclExpression({
|
||||||
value,
|
value,
|
||||||
initialVariableName: valueName = '',
|
initialVariableName: valueName = '',
|
||||||
|
sourceRange,
|
||||||
}: {
|
}: {
|
||||||
value: string
|
value: string
|
||||||
initialVariableName?: string
|
initialVariableName?: string
|
||||||
|
sourceRange?: SourceRange
|
||||||
}): {
|
}): {
|
||||||
inputRef: React.RefObject<HTMLInputElement>
|
inputRef: React.RefObject<HTMLInputElement>
|
||||||
valueNode: Expr | null
|
valueNode: Expr | null
|
||||||
@ -41,6 +43,9 @@ export function useCalculateKclExpression({
|
|||||||
const selectionRange:
|
const selectionRange:
|
||||||
| (typeof context)['selectionRanges']['graphSelections'][number]['codeRef']['range']
|
| (typeof context)['selectionRanges']['graphSelections'][number]['codeRef']['range']
|
||||||
| undefined = context.selectionRanges.graphSelections[0]?.codeRef?.range
|
| undefined = context.selectionRanges.graphSelections[0]?.codeRef?.range
|
||||||
|
// If there is no selection, use the end of the code
|
||||||
|
const endingSourceRange = sourceRange ||
|
||||||
|
selectionRange || [code.length, code.length]
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [availableVarInfo, setAvailableVarInfo] = useState<
|
const [availableVarInfo, setAvailableVarInfo] = useState<
|
||||||
ReturnType<typeof findAllPreviousVariables>
|
ReturnType<typeof findAllPreviousVariables>
|
||||||
@ -94,11 +99,10 @@ export function useCalculateKclExpression({
|
|||||||
const varInfo = findAllPreviousVariables(
|
const varInfo = findAllPreviousVariables(
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
kclManager.variables,
|
kclManager.variables,
|
||||||
// If there is no selection, use the end of the code
|
endingSourceRange
|
||||||
selectionRange || [code.length, code.length]
|
|
||||||
)
|
)
|
||||||
setAvailableVarInfo(varInfo)
|
setAvailableVarInfo(varInfo)
|
||||||
}, [kclManager.ast, kclManager.variables, selectionRange])
|
}, [kclManager.ast, kclManager.variables, endingSourceRange])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const execAstAndSetResult = async () => {
|
const execAstAndSetResult = async () => {
|
||||||
|
@ -250,6 +250,7 @@ export const commandBarMachine = setup({
|
|||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
'Command needs review': ({ context }) =>
|
'Command needs review': ({ context }) =>
|
||||||
|
// Edit flows are (for now) always considered to need review
|
||||||
context.selectedCommand?.needsReview ||
|
context.selectedCommand?.needsReview ||
|
||||||
('nodeToEdit' in context.argumentsToSubmit &&
|
('nodeToEdit' in context.argumentsToSubmit &&
|
||||||
context.argumentsToSubmit.nodeToEdit !== undefined) ||
|
context.argumentsToSubmit.nodeToEdit !== undefined) ||
|
||||||
@ -376,7 +377,18 @@ export const commandBarMachine = setup({
|
|||||||
argConfig.machineActor?.getSnapshot().context
|
argConfig.machineActor?.getSnapshot().context
|
||||||
)
|
)
|
||||||
: argConfig.options
|
: argConfig.options
|
||||||
).some((o) => o.value === argValue)
|
).some((o) => {
|
||||||
|
// Objects are only equal by reference in JavaScript, so we compare stringified values.
|
||||||
|
// GOTCHA: this means that JS class instances will behave badly as option arg values I believe.
|
||||||
|
if (
|
||||||
|
typeof o.value === 'object' &&
|
||||||
|
typeof argValue === 'object'
|
||||||
|
) {
|
||||||
|
return JSON.stringify(o.value) === JSON.stringify(argValue)
|
||||||
|
} else {
|
||||||
|
return o.value === argValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasMismatchedDefaultValueType ||
|
hasMismatchedDefaultValueType ||
|
||||||
|
@ -97,6 +97,8 @@ import { createProfileStartHandle } from 'clientSideScene/segments'
|
|||||||
import { DRAFT_POINT } from 'clientSideScene/sceneInfra'
|
import { DRAFT_POINT } from 'clientSideScene/sceneInfra'
|
||||||
import { setAppearance } from 'lang/modifyAst/setAppearance'
|
import { setAppearance } from 'lang/modifyAst/setAppearance'
|
||||||
import { DRAFT_DASHED_LINE } from 'clientSideScene/sceneEntities'
|
import { DRAFT_DASHED_LINE } from 'clientSideScene/sceneEntities'
|
||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { updateModelingState } from 'lang/modelingWorkflows'
|
||||||
|
|
||||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||||
|
|
||||||
@ -311,6 +313,10 @@ export type ModelingMachineEvent =
|
|||||||
type: 'event.parameter.create'
|
type: 'event.parameter.create'
|
||||||
data: ModelingCommandSchema['event.parameter.create']
|
data: ModelingCommandSchema['event.parameter.create']
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'event.parameter.edit'
|
||||||
|
data: ModelingCommandSchema['event.parameter.edit']
|
||||||
|
}
|
||||||
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||||||
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
||||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||||
@ -2323,6 +2329,39 @@ export const modelingMachine = setup({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
'actor.parameter.edit': fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['event.parameter.edit'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) return new Error('No input provided')
|
||||||
|
// Get the variable AST node to edit
|
||||||
|
const { nodeToEdit, value } = input
|
||||||
|
const newAst = structuredClone(kclManager.ast)
|
||||||
|
const variableNode = getNodeFromPath<Node<VariableDeclarator>>(
|
||||||
|
newAst,
|
||||||
|
nodeToEdit
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
err(variableNode) ||
|
||||||
|
variableNode.node.type !== 'VariableDeclarator' ||
|
||||||
|
!variableNode.node
|
||||||
|
) {
|
||||||
|
return new Error('No variable found, this is a bug')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutate the variable's value
|
||||||
|
variableNode.node.init = value.valueAst
|
||||||
|
|
||||||
|
await updateModelingState(newAst, {
|
||||||
|
codeManager,
|
||||||
|
editorManager,
|
||||||
|
kclManager,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
'set-up-draft-circle': fromPromise(
|
'set-up-draft-circle': fromPromise(
|
||||||
async (_: {
|
async (_: {
|
||||||
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
|
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
|
||||||
@ -2574,6 +2613,9 @@ export const modelingMachine = setup({
|
|||||||
'event.parameter.create': {
|
'event.parameter.create': {
|
||||||
target: '#Modeling.parameter.creating',
|
target: '#Modeling.parameter.creating',
|
||||||
},
|
},
|
||||||
|
'event.parameter.edit': {
|
||||||
|
target: '#Modeling.parameter.editing',
|
||||||
|
},
|
||||||
|
|
||||||
Export: {
|
Export: {
|
||||||
target: 'Exporting',
|
target: 'Exporting',
|
||||||
@ -3893,6 +3935,18 @@ export const modelingMachine = setup({
|
|||||||
onError: ['#Modeling.idle'],
|
onError: ['#Modeling.idle'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
editing: {
|
||||||
|
invoke: {
|
||||||
|
src: 'actor.parameter.edit',
|
||||||
|
id: 'actor.parameter.edit',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'event.parameter.edit') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['#Modeling.idle'],
|
||||||
|
onError: ['#Modeling.idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user