Add ability to create named constant without code (#5840)
* 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!
This commit is contained in:
@ -2,7 +2,7 @@ import { test, expect } from './zoo-test'
|
|||||||
import * as fsp from 'fs/promises'
|
import * as fsp from 'fs/promises'
|
||||||
import { executorInputPath, getUtils } from './test-utils'
|
import { executorInputPath, getUtils } from './test-utils'
|
||||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||||
import path from 'path'
|
import path, { join } from 'path'
|
||||||
|
|
||||||
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
||||||
test('Extrude from command bar selects extrude line after', async ({
|
test('Extrude from command bar selects extrude line after', async ({
|
||||||
@ -487,4 +487,53 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
|||||||
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
|
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Can add a named parameter or constant`, async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
context,
|
||||||
|
cmdBar,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
}) => {
|
||||||
|
const projectName = 'test'
|
||||||
|
const beforeKclCode = `a = 5
|
||||||
|
b = a * a
|
||||||
|
c = 3 + a`
|
||||||
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
const testProject = join(dir, projectName)
|
||||||
|
await fsp.mkdir(testProject, { recursive: true })
|
||||||
|
await fsp.writeFile(join(testProject, 'main.kcl'), beforeKclCode, 'utf-8')
|
||||||
|
})
|
||||||
|
await homePage.openProject(projectName)
|
||||||
|
// TODO: you probably shouldn't need an engine connection to add a parameter,
|
||||||
|
// but you do because all modeling commands have that requirement
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await test.step(`Go through the command palette flow`, async () => {
|
||||||
|
await cmdBar.cmdBarOpenBtn.click()
|
||||||
|
await cmdBar.chooseCommand('create parameter')
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Create parameter',
|
||||||
|
currentArgKey: 'value',
|
||||||
|
currentArgValue: '5',
|
||||||
|
headerArguments: {
|
||||||
|
Value: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'value',
|
||||||
|
})
|
||||||
|
await cmdBar.argumentInput.locator('[contenteditable]').fill(`b - 5`)
|
||||||
|
// TODO: we have no loading indicator for the KCL argument input calculation
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'commandBarClosed',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`a = 5b = a * amyParameter001 = b - 5c = 3 + a`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -231,9 +231,9 @@ test.describe('Feature Tree pane', () => {
|
|||||||
|> circle(center = [0, 0], radius = 5)
|
|> circle(center = [0, 0], radius = 5)
|
||||||
renamedExtrude = extrude(sketch001, length = ${initialInput})`
|
renamedExtrude = extrude(sketch001, length = ${initialInput})`
|
||||||
const newConstantName = 'distance001'
|
const newConstantName = 'distance001'
|
||||||
const expectedCode = `sketch001 = startSketchOn('XZ')
|
const expectedCode = `${newConstantName} = 23
|
||||||
|
sketch001 = startSketchOn('XZ')
|
||||||
|> circle(center = [0, 0], radius = 5)
|
|> circle(center = [0, 0], radius = 5)
|
||||||
${newConstantName} = 23
|
|
||||||
renamedExtrude = extrude(sketch001, length = ${newConstantName})`
|
renamedExtrude = extrude(sketch001, length = ${newConstantName})`
|
||||||
|
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Binary file not shown.
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
@ -114,7 +114,7 @@ export const CommandBar = () => {
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<WrapperComponent.Panel
|
<WrapperComponent.Panel
|
||||||
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded rounded-tl-none shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
className="relative z-50 pointer-events-auto w-full max-w-xl pt-2 mx-auto border rounded rounded-tl-none shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||||
as="div"
|
as="div"
|
||||||
data-testid="command-bar"
|
data-testid="command-bar"
|
||||||
>
|
>
|
||||||
|
@ -81,7 +81,8 @@ function CommandBarKclInput({
|
|||||||
const [value, setValue] = useState(initialValue)
|
const [value, setValue] = useState(initialValue)
|
||||||
const [createNewVariable, setCreateNewVariable] = useState(
|
const [createNewVariable, setCreateNewVariable] = useState(
|
||||||
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
||||||
arg.createVariableByDefault ||
|
arg.createVariable === 'byDefault' ||
|
||||||
|
arg.createVariable === 'force' ||
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
@ -248,7 +249,7 @@ function CommandBarKclInput({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{createNewVariable ? (
|
{createNewVariable ? (
|
||||||
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
|
<div className="flex mb-2 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"
|
||||||
@ -269,7 +270,11 @@ function CommandBarKclInput({
|
|||||||
autoFocus
|
autoFocus
|
||||||
onChange={(e) => setNewVariableName(e.target.value)}
|
onChange={(e) => setNewVariableName(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.currentTarget.value === '' && e.key === 'Backspace') {
|
if (
|
||||||
|
e.currentTarget.value === '' &&
|
||||||
|
e.key === 'Backspace' &&
|
||||||
|
arg.createVariable !== 'force'
|
||||||
|
) {
|
||||||
setCreateNewVariable(false)
|
setCreateNewVariable(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -290,6 +295,7 @@ function CommandBarKclInput({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
arg.createVariable !== 'disallow' && (
|
||||||
<div className="flex justify-between gap-2 px-4">
|
<div className="flex justify-between gap-2 px-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCreateNewVariable(true)}
|
onClick={() => setCreateNewVariable(true)}
|
||||||
@ -299,6 +305,7 @@ function CommandBarKclInput({
|
|||||||
Create new variable
|
Create new variable
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
import { Menu } from '@headlessui/react'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
|
import styles from './KclEditorMenu.module.css'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
|
export const FeatureTreeMenu = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
if (e.eventPhase === 3 && target.closest('a') === null) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu.Button className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 ui-open:!border-primary dark:ui-open:!border-chalkboard-70 !outline-none">
|
||||||
|
<ActionIcon
|
||||||
|
icon="three-dots"
|
||||||
|
className="p-1"
|
||||||
|
size="sm"
|
||||||
|
bgClassName="bg-transparent dark:bg-transparent"
|
||||||
|
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'modeling',
|
||||||
|
name: 'event.parameter.create',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
<span>Create parameter</span>
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
@ -19,6 +19,7 @@ import { ContextFrom } from 'xstate'
|
|||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { FeatureTreePane } from './FeatureTreePane'
|
import { FeatureTreePane } from './FeatureTreePane'
|
||||||
import { kclErrorsByFilename } from 'lang/errors'
|
import { kclErrorsByFilename } from 'lang/errors'
|
||||||
|
import { FeatureTreeMenu } from './FeatureTreeMenu'
|
||||||
|
|
||||||
export type SidebarType =
|
export type SidebarType =
|
||||||
| 'code'
|
| 'code'
|
||||||
@ -85,6 +86,7 @@ export const sidebarPanes: SidebarPane[] = [
|
|||||||
id={props.id}
|
id={props.id}
|
||||||
icon="model"
|
icon="model"
|
||||||
title="Feature Tree"
|
title="Feature Tree"
|
||||||
|
Menu={FeatureTreeMenu}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
/>
|
/>
|
||||||
<FeatureTreePane />
|
<FeatureTreePane />
|
||||||
|
@ -223,8 +223,8 @@ export function findUniqueName(
|
|||||||
if (!nameIsInString) return name
|
if (!nameIsInString) return name
|
||||||
|
|
||||||
// recursive case: name is not unique and ends in digits
|
// recursive case: name is not unique and ends in digits
|
||||||
const newPad = nameEndsInDigits[1].length
|
const newPad = nameEndsInDigits[0].length
|
||||||
const newIndex = parseInt(nameEndsInDigits[1]) + 1
|
const newIndex = parseInt(nameEndsInDigits[0]) + 1
|
||||||
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
||||||
|
|
||||||
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
||||||
|
@ -201,6 +201,11 @@ export function traverse(
|
|||||||
])
|
])
|
||||||
} else if (_node.type === 'VariableDeclarator') {
|
} else if (_node.type === 'VariableDeclarator') {
|
||||||
_traverse(_node.init, [...pathToNode, ['init', '']])
|
_traverse(_node.init, [...pathToNode, ['init', '']])
|
||||||
|
} else if (_node.type === 'ExpressionStatement') {
|
||||||
|
_traverse(_node.expression, [
|
||||||
|
...pathToNode,
|
||||||
|
['expression', 'ExpressionStatement'],
|
||||||
|
])
|
||||||
} else if (_node.type === 'PipeExpression') {
|
} else if (_node.type === 'PipeExpression') {
|
||||||
_node.body.forEach((expression, index) =>
|
_node.body.forEach((expression, index) =>
|
||||||
_traverse(expression, [
|
_traverse(expression, [
|
||||||
|
44
src/lang/queryAst/getIdentifiersInProgram.test.ts
Normal file
44
src/lang/queryAst/getIdentifiersInProgram.test.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { assertParse, initPromise } from 'lang/wasm'
|
||||||
|
import { getIdentifiersInProgram } from './getIndentifiersInProgram'
|
||||||
|
|
||||||
|
function identifier(name: string, start: number, end: number) {
|
||||||
|
return {
|
||||||
|
type: 'Identifier',
|
||||||
|
name,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initPromise
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`getIdentifiersInProgram`, () => {
|
||||||
|
it(`finds no identifiers in an empty program`, () => {
|
||||||
|
const identifiers = getIdentifiersInProgram(assertParse(''))
|
||||||
|
expect(identifiers).toEqual([])
|
||||||
|
})
|
||||||
|
it(`finds a single identifier in an expression`, () => {
|
||||||
|
const identifiers = getIdentifiersInProgram(assertParse('55 + a'))
|
||||||
|
expect(identifiers).toEqual([identifier('a', 5, 6)])
|
||||||
|
})
|
||||||
|
it(`finds multiple identifiers in an expression`, () => {
|
||||||
|
const identifiers = getIdentifiersInProgram(assertParse('a + b + c'))
|
||||||
|
expect(identifiers).toEqual([
|
||||||
|
identifier('a', 0, 1),
|
||||||
|
identifier('b', 4, 5),
|
||||||
|
identifier('c', 8, 9),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
it(`finds all the identifiers in a normal program`, () => {
|
||||||
|
const program = assertParse(`x = 5 + 2
|
||||||
|
y = x * 2
|
||||||
|
z = y + 1`)
|
||||||
|
const identifiers = getIdentifiersInProgram(program)
|
||||||
|
expect(identifiers).toEqual([
|
||||||
|
identifier('x', 14, 15),
|
||||||
|
identifier('y', 24, 25),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
21
src/lang/queryAst/getIndentifiersInProgram.ts
Normal file
21
src/lang/queryAst/getIndentifiersInProgram.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { traverse } from 'lang/queryAst'
|
||||||
|
import { Expr, Identifier, Program } from 'lang/wasm'
|
||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an AST `Program`, return an array of
|
||||||
|
* all the `Identifier` nodes within.
|
||||||
|
*/
|
||||||
|
export function getIdentifiersInProgram(
|
||||||
|
program: Node<Program | Expr>
|
||||||
|
): Identifier[] {
|
||||||
|
const identifiers: Identifier[] = []
|
||||||
|
traverse(program, {
|
||||||
|
enter(node) {
|
||||||
|
if (node.type === 'Identifier') {
|
||||||
|
identifiers.push(node)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return identifiers
|
||||||
|
}
|
64
src/lang/queryAst/getSafeInsertIndex.test.ts
Normal file
64
src/lang/queryAst/getSafeInsertIndex.test.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { assertParse, initPromise } from 'lang/wasm'
|
||||||
|
import { getSafeInsertIndex } from './getSafeInsertIndex'
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initPromise
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`getSafeInsertIndex`, () => {
|
||||||
|
it(`expression with no identifiers`, () => {
|
||||||
|
const baseProgram = assertParse(`x = 5 + 2
|
||||||
|
y = 2
|
||||||
|
z = x + y`)
|
||||||
|
const targetExpr = assertParse(`5`)
|
||||||
|
expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0)
|
||||||
|
})
|
||||||
|
it(`expression with no identifiers in longer program`, () => {
|
||||||
|
const baseProgram = assertParse(`x = 5 + 2
|
||||||
|
profile001 = startProfileAt([0.07, 0], sketch001)
|
||||||
|
|> angledLine([0, x], %, $a)
|
||||||
|
|> angledLine([segAng(a) + 90, 5], %)
|
||||||
|
|> angledLine([segAng(a), -segLen(a)], %)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()`)
|
||||||
|
const targetExpr = assertParse(`5`)
|
||||||
|
expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0)
|
||||||
|
})
|
||||||
|
it(`expression with an identifier in the middle`, () => {
|
||||||
|
const baseProgram = assertParse(`x = 5 + 2
|
||||||
|
y = 2
|
||||||
|
z = x + y`)
|
||||||
|
const targetExpr = assertParse(`5 + y`)
|
||||||
|
expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2)
|
||||||
|
})
|
||||||
|
it(`expression with an identifier at the end`, () => {
|
||||||
|
const baseProgram = assertParse(`x = 5 + 2
|
||||||
|
y = 2
|
||||||
|
z = x + y`)
|
||||||
|
const targetExpr = assertParse(`z * z`)
|
||||||
|
expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(3)
|
||||||
|
})
|
||||||
|
it(`expression with a tag declarator add to end`, () => {
|
||||||
|
const baseProgram = assertParse(`x = 5 + 2
|
||||||
|
profile001 = startProfileAt([0.07, 0], sketch001)
|
||||||
|
|> angledLine([0, x], %, $a)
|
||||||
|
|> angledLine([segAng(a) + 90, 5], %)
|
||||||
|
|> angledLine([segAng(a), -segLen(a)], %)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()`)
|
||||||
|
const targetExpr = assertParse(`5 + segAng(a)`)
|
||||||
|
expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2)
|
||||||
|
})
|
||||||
|
it(`expression with a tag declarator and variable in the middle`, () => {
|
||||||
|
const baseProgram = assertParse(`x = 5 + 2
|
||||||
|
profile001 = startProfileAt([0.07, 0], sketch001)
|
||||||
|
|> angledLine([0, x], %, $a)
|
||||||
|
|> angledLine([segAng(a) + 90, 5], %)
|
||||||
|
|> angledLine([segAng(a), -segLen(a)], %)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
y = x + x`)
|
||||||
|
const targetExpr = assertParse(`x + segAng(a)`)
|
||||||
|
expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
36
src/lang/queryAst/getSafeInsertIndex.ts
Normal file
36
src/lang/queryAst/getSafeInsertIndex.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { getIdentifiersInProgram } from './getIndentifiersInProgram'
|
||||||
|
import { Program, Expr } from 'lang/wasm'
|
||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { getTagDeclaratorsInProgram } from './getTagDeclaratorsInProgram'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a target expression, return the body index of the last-used variable
|
||||||
|
* or tag declaration within the provided program.
|
||||||
|
*/
|
||||||
|
export function getSafeInsertIndex(
|
||||||
|
targetExpr: Node<Program | Expr>,
|
||||||
|
program: Node<Program>
|
||||||
|
) {
|
||||||
|
const identifiers = getIdentifiersInProgram(targetExpr)
|
||||||
|
const safeIdentifierIndex = identifiers.reduce((acc, curr) => {
|
||||||
|
const bodyIndex = program.body.findIndex(
|
||||||
|
(a) =>
|
||||||
|
a.type === 'VariableDeclaration' && a.declaration.id?.name === curr.name
|
||||||
|
)
|
||||||
|
return Math.max(acc, bodyIndex + 1)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const tagDeclarators = getTagDeclaratorsInProgram(program)
|
||||||
|
console.log('FRANK tagDeclarators', {
|
||||||
|
identifiers,
|
||||||
|
tagDeclarators,
|
||||||
|
targetExpr,
|
||||||
|
})
|
||||||
|
const safeTagIndex = tagDeclarators.reduce((acc, curr) => {
|
||||||
|
return identifiers.findIndex((a) => a.name === curr.tag.value) === -1
|
||||||
|
? acc
|
||||||
|
: Math.max(acc, curr.bodyIndex + 1)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return Math.max(safeIdentifierIndex, safeTagIndex)
|
||||||
|
}
|
68
src/lang/queryAst/getTagDeclaratorsInProgram.test.ts
Normal file
68
src/lang/queryAst/getTagDeclaratorsInProgram.test.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { assertParse, initPromise } from 'lang/wasm'
|
||||||
|
import { getTagDeclaratorsInProgram } from './getTagDeclaratorsInProgram'
|
||||||
|
|
||||||
|
function tagDeclaratorWithIndex(
|
||||||
|
value: string,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
bodyIndex: number
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
tag: {
|
||||||
|
type: 'TagDeclarator',
|
||||||
|
value,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
bodyIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initPromise
|
||||||
|
})
|
||||||
|
|
||||||
|
describe(`getTagDeclaratorsInProgram`, () => {
|
||||||
|
it(`finds no tag declarators in an empty program`, () => {
|
||||||
|
const tagDeclarators = getTagDeclaratorsInProgram(assertParse(''))
|
||||||
|
expect(tagDeclarators).toEqual([])
|
||||||
|
})
|
||||||
|
it(`finds a single tag declarators in a small program`, () => {
|
||||||
|
const tagDeclarators = getTagDeclaratorsInProgram(
|
||||||
|
assertParse(`sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([0, 0], sketch001)
|
||||||
|
|> angledLine([0, 11], %, $a)`)
|
||||||
|
)
|
||||||
|
expect(tagDeclarators).toEqual([tagDeclaratorWithIndex('a', 107, 109, 1)])
|
||||||
|
})
|
||||||
|
it(`finds multiple tag declarators in a small program`, () => {
|
||||||
|
const program = `sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([0.07, 0], sketch001)
|
||||||
|
|> angledLine([0, 11], %, $a)
|
||||||
|
|> angledLine([segAng(a) + 90, 11.17], %, $b)
|
||||||
|
|> angledLine([segAng(a), -segLen(a)], %, $c)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()`
|
||||||
|
const tagDeclarators = getTagDeclaratorsInProgram(assertParse(program))
|
||||||
|
expect(tagDeclarators).toEqual([
|
||||||
|
tagDeclaratorWithIndex('a', 110, 112, 1),
|
||||||
|
tagDeclaratorWithIndex('b', 158, 160, 1),
|
||||||
|
tagDeclaratorWithIndex('c', 206, 208, 1),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
it(`finds tag declarators at different indices`, () => {
|
||||||
|
const program = `sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([0.07, 0], sketch001)
|
||||||
|
|> angledLine([0, 11], %, $a)
|
||||||
|
profile002 = angledLine([segAng(a) + 90, 11.17], profile001, $b)
|
||||||
|
|> angledLine([segAng(a), -segLen(a)], %, $c)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()`
|
||||||
|
const tagDeclarators = getTagDeclaratorsInProgram(assertParse(program))
|
||||||
|
expect(tagDeclarators).toEqual([
|
||||||
|
tagDeclaratorWithIndex('a', 110, 112, 1),
|
||||||
|
tagDeclaratorWithIndex('b', 175, 177, 2),
|
||||||
|
tagDeclaratorWithIndex('c', 223, 225, 2),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
28
src/lang/queryAst/getTagDeclaratorsInProgram.ts
Normal file
28
src/lang/queryAst/getTagDeclaratorsInProgram.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { getBodyIndex, traverse } from 'lang/queryAst'
|
||||||
|
import { Expr, Program } from 'lang/wasm'
|
||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { TagDeclarator } from '@rust/kcl-lib/bindings/TagDeclarator'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
|
|
||||||
|
type TagWithBodyIndex = { tag: TagDeclarator; bodyIndex: number }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an AST `Program`, return an array of
|
||||||
|
* TagDeclarator nodes within, and the body index of their source expression.
|
||||||
|
*/
|
||||||
|
export function getTagDeclaratorsInProgram(
|
||||||
|
program: Node<Program | Expr>
|
||||||
|
): TagWithBodyIndex[] {
|
||||||
|
const tagLocations: TagWithBodyIndex[] = []
|
||||||
|
traverse(program, {
|
||||||
|
enter(node, pathToNode) {
|
||||||
|
if (node.type === 'TagDeclarator') {
|
||||||
|
// Get the body index of the declarator's farthest ancestor
|
||||||
|
const bodyIndex = getBodyIndex(pathToNode)
|
||||||
|
if (err(bodyIndex)) return
|
||||||
|
tagLocations.push({ tag: node, bodyIndex })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return tagLocations
|
||||||
|
}
|
@ -92,6 +92,9 @@ export type ModelingCommandSchema = {
|
|||||||
axis: string
|
axis: string
|
||||||
length: KclCommandValue
|
length: KclCommandValue
|
||||||
}
|
}
|
||||||
|
'event.parameter.create': {
|
||||||
|
value: KclCommandValue
|
||||||
|
}
|
||||||
'change tool': {
|
'change tool': {
|
||||||
tool: SketchTool
|
tool: SketchTool
|
||||||
}
|
}
|
||||||
@ -582,6 +585,22 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'event.parameter.create': {
|
||||||
|
displayName: 'Create parameter',
|
||||||
|
description: 'Add a named constant to use in geometry',
|
||||||
|
icon: 'make-variable',
|
||||||
|
status: 'development',
|
||||||
|
needsReview: false,
|
||||||
|
args: {
|
||||||
|
value: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: true,
|
||||||
|
createVariable: 'force',
|
||||||
|
variableName: 'myParameter',
|
||||||
|
defaultValue: '5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'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',
|
||||||
@ -596,7 +615,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
length: {
|
length: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
required: true,
|
required: true,
|
||||||
createVariableByDefault: true,
|
createVariable: 'byDefault',
|
||||||
defaultValue(_, machineContext) {
|
defaultValue(_, machineContext) {
|
||||||
const selectionRanges = machineContext?.selectionRanges
|
const selectionRanges = machineContext?.selectionRanges
|
||||||
if (!selectionRanges) return KCL_DEFAULT_LENGTH
|
if (!selectionRanges) return KCL_DEFAULT_LENGTH
|
||||||
@ -636,7 +655,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
namedValue: {
|
namedValue: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
required: true,
|
required: true,
|
||||||
createVariableByDefault: true,
|
createVariable: 'byDefault',
|
||||||
variableName(commandBarContext, machineContext) {
|
variableName(commandBarContext, machineContext) {
|
||||||
const { currentValue } = commandBarContext.argumentsToSubmit
|
const { currentValue } = commandBarContext.argumentsToSubmit
|
||||||
if (
|
if (
|
||||||
|
@ -186,7 +186,7 @@ export type CommandArgumentConfig<
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: 'kcl'
|
inputType: 'kcl'
|
||||||
createVariableByDefault?: boolean
|
createVariable?: 'byDefault' | 'force' | 'disallow'
|
||||||
variableName?:
|
variableName?:
|
||||||
| string
|
| string
|
||||||
| ((
|
| ((
|
||||||
@ -306,7 +306,7 @@ export type CommandArgument<
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: 'kcl'
|
inputType: 'kcl'
|
||||||
createVariableByDefault?: boolean
|
createVariable?: 'byDefault' | 'force' | 'disallow'
|
||||||
variableName?:
|
variableName?:
|
||||||
| string
|
| string
|
||||||
| ((
|
| ((
|
||||||
|
@ -201,7 +201,7 @@ export function buildCommandArgument<
|
|||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
createVariableByDefault: arg.createVariableByDefault,
|
createVariable: arg.createVariable,
|
||||||
variableName: arg.variableName,
|
variableName: arg.variableName,
|
||||||
defaultValue: arg.defaultValue,
|
defaultValue: arg.defaultValue,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
|
@ -8,6 +8,7 @@ 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'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { getSafeInsertIndex } from 'lang/queryAst/getSafeInsertIndex'
|
||||||
|
|
||||||
const isValidVariableName = (name: string) =>
|
const isValidVariableName = (name: string) =>
|
||||||
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||||
@ -48,6 +49,7 @@ export function useCalculateKclExpression({
|
|||||||
insertIndex: 0,
|
insertIndex: 0,
|
||||||
bodyPath: [],
|
bodyPath: [],
|
||||||
})
|
})
|
||||||
|
const [insertIndex, setInsertIndex] = useState(0)
|
||||||
const [valueNode, setValueNode] = useState<Expr | null>(null)
|
const [valueNode, setValueNode] = useState<Expr | null>(null)
|
||||||
// Gotcha: If we do not attempt to parse numeric literals instantly it means that there is an async action to verify
|
// Gotcha: If we do not attempt to parse numeric literals instantly it means that there is an async action to verify
|
||||||
// the value is good. This means all E2E tests have a race condition on when they can hit "next" in the command bar.
|
// the value is good. This means all E2E tests have a race condition on when they can hit "next" in the command bar.
|
||||||
@ -101,11 +103,13 @@ export function useCalculateKclExpression({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const execAstAndSetResult = async () => {
|
const execAstAndSetResult = async () => {
|
||||||
const result = await getCalculatedKclExpressionValue(value)
|
const result = await getCalculatedKclExpressionValue(value)
|
||||||
if (result instanceof Error || 'errors' in result) {
|
if (result instanceof Error || 'errors' in result || !result.astNode) {
|
||||||
setCalcResult('NAN')
|
setCalcResult('NAN')
|
||||||
setValueNode(null)
|
setValueNode(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const newInsertIndex = getSafeInsertIndex(result.astNode, kclManager.ast)
|
||||||
|
setInsertIndex(newInsertIndex)
|
||||||
setCalcResult(result?.valueAsString || 'NAN')
|
setCalcResult(result?.valueAsString || 'NAN')
|
||||||
result?.astNode && setValueNode(result.astNode)
|
result?.astNode && setValueNode(result.astNode)
|
||||||
}
|
}
|
||||||
@ -120,7 +124,7 @@ export function useCalculateKclExpression({
|
|||||||
valueNode,
|
valueNode,
|
||||||
calcResult,
|
calcResult,
|
||||||
prevVariables: availableVarInfo.variables,
|
prevVariables: availableVarInfo.variables,
|
||||||
newVariableInsertIndex: availableVarInfo.insertIndex,
|
newVariableInsertIndex: insertIndex,
|
||||||
newVariableName,
|
newVariableName,
|
||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
|
@ -50,6 +50,7 @@ import {
|
|||||||
createIdentifier,
|
createIdentifier,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
extrudeSketch,
|
extrudeSketch,
|
||||||
|
insertNamedConstant,
|
||||||
loftSketches,
|
loftSketches,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
@ -306,6 +307,10 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Constrain parallel' }
|
| { type: 'Constrain parallel' }
|
||||||
| { type: 'Constrain remove constraints'; data?: PathToNode }
|
| { type: 'Constrain remove constraints'; data?: PathToNode }
|
||||||
| { type: 'Re-execute' }
|
| { type: 'Re-execute' }
|
||||||
|
| {
|
||||||
|
type: 'event.parameter.create'
|
||||||
|
data: ModelingCommandSchema['event.parameter.create']
|
||||||
|
}
|
||||||
| { 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'] }
|
||||||
@ -2291,6 +2296,32 @@ export const modelingMachine = setup({
|
|||||||
if (err(filletResult)) return filletResult
|
if (err(filletResult)) return filletResult
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
'actor.parameter.create': fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['event.parameter.create'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) return new Error('No input provided')
|
||||||
|
const { value } = input
|
||||||
|
if (!('variableName' in value)) {
|
||||||
|
return new Error('variable name is required')
|
||||||
|
}
|
||||||
|
const newAst = insertNamedConstant({
|
||||||
|
node: kclManager.ast,
|
||||||
|
newExpression: value,
|
||||||
|
})
|
||||||
|
const updateAstResult = await kclManager.updateAst(newAst, true)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updateAstResult.newAst
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updateAstResult?.selections) {
|
||||||
|
editorManager.selectRange(updateAstResult?.selections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
'set-up-draft-circle': fromPromise(
|
'set-up-draft-circle': fromPromise(
|
||||||
async (_: {
|
async (_: {
|
||||||
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
|
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
|
||||||
@ -2539,6 +2570,10 @@ export const modelingMachine = setup({
|
|||||||
reenter: true,
|
reenter: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'event.parameter.create': {
|
||||||
|
target: '#Modeling.parameter.creating',
|
||||||
|
},
|
||||||
|
|
||||||
Export: {
|
Export: {
|
||||||
target: 'Exporting',
|
target: 'Exporting',
|
||||||
guard: 'Has exportable geometry',
|
guard: 'Has exportable geometry',
|
||||||
@ -3839,6 +3874,24 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parameter: {
|
||||||
|
type: 'parallel',
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
invoke: {
|
||||||
|
src: 'actor.parameter.create',
|
||||||
|
id: 'actor.parameter.create',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'event.parameter.create') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['#Modeling.idle'],
|
||||||
|
onError: ['#Modeling.idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
'Applying Prompt-to-edit': {
|
'Applying Prompt-to-edit': {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'submit-prompt-edit',
|
src: 'submit-prompt-edit',
|
||||||
|
Reference in New Issue
Block a user