Replace number command bar arg input type with kcl expression input (#1474)
* Rename useCalc * Move CommandBar so it has access to settings and kcl * Create codemirror variable mention extension * Make project path a dep of TextEditor useMemo * Add incomplete KCL input for CommandBar to replace current number arg type * Add previous variables autocompletion to kcl input * Fix missed typos from merge * Working AST mods, not working variable additions * Add ability to create a new variable * Add icon and tooltip to command arg tag if a variable is added * Polish variable naming logic, preserve when going back * Allow stepping back from KCL input * Don't prevent keydown of enter, it's used by autocomplete * Round the variable value in cmd bar header * Add Playwright test * Formatting, polish TS types * More type wrangling * Needed to fmt after above type wrangling * Update snapshot tests to account for new variable name autogeneration * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Merge branch 'main' into cmd-bar-make-variable * Update all test instances of var name with number index after merge with main * Partial revert of "Polish variable naming logic, preserve when going back" This reverts commitdddcb13c36
. * Revert "Update all test instances of var name with number index after merge with main" This reverts commit8c4b63b523
. * Revert "Update snapshot tests to account for new variable name autogeneration" This reverts commit11bfce3832
. * Retry a refactoring of findUniqueName * minor feedback from @jgomez720 - better highlighting of kcl input - consistent hotkeys - disallow invalid var names * Polish stepping back state logic * Fix tests now that keyboard shortcut changed * Remove unused imports * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Fix tests * Trigger CI * Update src/components/ProjectSidebarMenu.test.tsx * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * re-trigger CI --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
@ -637,12 +637,15 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
|||||||
await context.addInitScript(async (token) => {
|
await context.addInitScript(async (token) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`
|
||||||
|
const distance = sqrt(20)
|
||||||
|
const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([25.1, 0.41], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> line([0.73, -14.93], %)
|
|> line([0.73, -14.93], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([-23.44, 0.52], %)
|
||||||
|> close(%)`
|
|> close(%)
|
||||||
|
`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -667,24 +670,42 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
|||||||
// Click to select face and set distance
|
// Click to select face and set distance
|
||||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||||
await page.getByRole('button', { name: 'Continue' }).click()
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
|
||||||
|
// Assert that we're on the distance step
|
||||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||||
await page.keyboard.press('Enter')
|
|
||||||
|
// Assert that the an alternative variable name is chosen,
|
||||||
|
// since the default variable name is already in use (distance)
|
||||||
|
await page.getByRole('button', { name: 'Create new variable' }).click()
|
||||||
|
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||||
|
'distance001'
|
||||||
|
)
|
||||||
|
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
|
||||||
// Review step and argument hotkeys
|
// Review step and argument hotkeys
|
||||||
await page.keyboard.press('2')
|
await expect(
|
||||||
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
|
page.getByRole('button', { name: 'Submit command' })
|
||||||
|
).toBeEnabled()
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Distance 12', exact: false })
|
||||||
|
).toBeDisabled()
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
// Check that the code was updated
|
// Check that the code was updated
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
// Unfortunately this indentation seems to matter for the test
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const distance = sqrt(20)
|
||||||
|
const distance001 = 5 + 7
|
||||||
|
const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([25.1, 0.41], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> line([0.73, -14.93], %)
|
|> line([0.73, -14.93], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([-23.44, 0.52], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(5, %)`
|
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Binary file not shown.
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@ -35,7 +35,9 @@ import {
|
|||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from './machines/settingsMachine'
|
} from './machines/settingsMachine'
|
||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
import CommandBarProvider, {
|
||||||
|
CommandBar,
|
||||||
|
} from 'components/CommandBar/CommandBar'
|
||||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
@ -117,6 +119,7 @@ const router = createBrowserRouter(
|
|||||||
<ModelingMachineProvider>
|
<ModelingMachineProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<App />
|
<App />
|
||||||
|
<CommandBar />
|
||||||
</ModelingMachineProvider>
|
</ModelingMachineProvider>
|
||||||
<WasmErrBanner />
|
<WasmErrBanner />
|
||||||
</FileMachineProvider>
|
</FileMachineProvider>
|
||||||
@ -216,6 +219,7 @@ const router = createBrowserRouter(
|
|||||||
<Auth>
|
<Auth>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<Home />
|
<Home />
|
||||||
|
<CommandBar />
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
loader: async (): Promise<HomeLoaderData | Response> => {
|
loader: async (): Promise<HomeLoaderData | Response> => {
|
||||||
|
@ -87,7 +87,7 @@ export function useCalc({
|
|||||||
inputRef: React.RefObject<HTMLInputElement>
|
inputRef: React.RefObject<HTMLInputElement>
|
||||||
valueNode: Value | null
|
valueNode: Value | null
|
||||||
calcResult: string
|
calcResult: string
|
||||||
prevVariables: PrevVariable<any>[]
|
prevVariables: PrevVariable<unknown>[]
|
||||||
newVariableName: string
|
newVariableName: string
|
||||||
isNewVariableNameUnique: boolean
|
isNewVariableNameUnique: boolean
|
||||||
newVariableInsertIndex: number
|
newVariableInsertIndex: number
|
||||||
|
@ -57,12 +57,11 @@ export const CommandBarProvider = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CommandBar />
|
|
||||||
</CommandsContext.Provider>
|
</CommandsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
@ -84,17 +83,25 @@ const CommandBar = () => {
|
|||||||
if (commandBarState.matches('Review')) {
|
if (commandBarState.matches('Review')) {
|
||||||
const entries = Object.entries(selectedCommand?.args || {})
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
|
|
||||||
commandBarSend({
|
const currentArgName = entries[entries.length - 1][0]
|
||||||
type: commandBarState.matches('Review')
|
const currentArg = {
|
||||||
? 'Edit argument'
|
name: currentArgName,
|
||||||
: 'Change current argument',
|
|
||||||
data: {
|
|
||||||
arg: {
|
|
||||||
name: entries[entries.length - 1][0],
|
|
||||||
...entries[entries.length - 1][1],
|
...entries[entries.length - 1][1],
|
||||||
},
|
}
|
||||||
|
|
||||||
|
if (commandBarState.matches('Review')) {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Edit argument',
|
||||||
|
data: {
|
||||||
|
arg: currentArg,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Remove argument',
|
||||||
|
data: { [currentArgName]: currentArg },
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarSend({ type: 'Deselect command' })
|
||||||
}
|
}
|
||||||
@ -117,6 +124,11 @@ const CommandBar = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => console.log(commandBarState.context.argumentsToSubmit),
|
||||||
|
[commandBarState.context.argumentsToSubmit]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root
|
<Transition.Root
|
||||||
show={!commandBarState.matches('Closed') || false}
|
show={!commandBarState.matches('Closed') || false}
|
||||||
|
@ -4,6 +4,7 @@ import CommandBarSelectionInput from './CommandBarSelectionInput'
|
|||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import CommandBarHeader from './CommandBarHeader'
|
import CommandBarHeader from './CommandBarHeader'
|
||||||
|
import CommandBarKclInput from './CommandBarKclInput'
|
||||||
|
|
||||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
@ -17,10 +18,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
|||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Submit argument',
|
type: 'Submit argument',
|
||||||
data: {
|
data: {
|
||||||
[currentArgument.name]:
|
[currentArgument.name]: data,
|
||||||
currentArgument.inputType === 'number'
|
|
||||||
? parseFloat((data as string) || '0')
|
|
||||||
: data,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -68,6 +66,10 @@ function ArgumentInput({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'kcl':
|
||||||
|
return (
|
||||||
|
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<CommandBarBasicInput
|
<CommandBarBasicInput
|
||||||
|
@ -9,7 +9,7 @@ function CommandBarBasicInput({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: {
|
}: {
|
||||||
arg: CommandArgument<unknown> & {
|
arg: CommandArgument<unknown> & {
|
||||||
inputType: 'number' | 'string'
|
inputType: 'string'
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
@ -18,7 +18,6 @@ function CommandBarBasicInput({
|
|||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const inputType = arg.inputType === 'number' ? 'number' : 'text'
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@ -40,9 +39,9 @@ function CommandBarBasicInput({
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
id="arg-form"
|
id="arg-form"
|
||||||
name={inputType}
|
name={arg.inputType}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type={inputType}
|
type={arg.inputType}
|
||||||
required
|
required
|
||||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
placeholder="Enter a value"
|
placeholder="Enter a value"
|
||||||
|
@ -4,6 +4,9 @@ import React, { ReactNode, useState } from 'react'
|
|||||||
import { ActionButton } from '../ActionButton'
|
import { ActionButton } from '../ActionButton'
|
||||||
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { roundOff } from 'lib/utils'
|
||||||
|
|
||||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
@ -45,6 +48,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
parseInt(b.keys[0], 10) - 1
|
parseInt(b.keys[0], 10) - 1
|
||||||
]
|
]
|
||||||
const arg = selectedCommand?.args[argName]
|
const arg = selectedCommand?.args[argName]
|
||||||
|
if (!argName || !arg) return
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Change current argument',
|
type: 'Change current argument',
|
||||||
data: { arg: { ...arg, name: argName } },
|
data: { arg: { ...arg, name: argName } },
|
||||||
@ -59,7 +63,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
selectedCommand &&
|
selectedCommand &&
|
||||||
argumentsToSubmit && (
|
argumentsToSubmit && (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 text-sm flex gap-4 items-start">
|
<div className="group px-4 text-sm flex gap-4 items-start">
|
||||||
<div className="flex flex-1 flex-wrap gap-2">
|
<div className="flex flex-1 flex-wrap gap-2">
|
||||||
<p
|
<p
|
||||||
data-command-name={selectedCommand?.name}
|
data-command-name={selectedCommand?.name}
|
||||||
@ -91,25 +95,50 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<span className="capitalize">{argName}</span>
|
||||||
{argumentsToSubmit[argName] ? (
|
{argumentsToSubmit[argName] ? (
|
||||||
arg.inputType === 'selection' ? (
|
arg.inputType === 'selection' ? (
|
||||||
getSelectionTypeDisplayText(
|
getSelectionTypeDisplayText(
|
||||||
argumentsToSubmit[argName] as Selections
|
argumentsToSubmit[argName] as Selections
|
||||||
)
|
)
|
||||||
|
) : arg.inputType === 'kcl' ? (
|
||||||
|
roundOff(
|
||||||
|
Number(
|
||||||
|
(argumentsToSubmit[argName] as KclCommandValue)
|
||||||
|
.valueCalculated
|
||||||
|
),
|
||||||
|
4
|
||||||
|
)
|
||||||
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
||||||
JSON.stringify(argumentsToSubmit[argName])
|
JSON.stringify(argumentsToSubmit[argName])
|
||||||
) : (
|
) : (
|
||||||
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
||||||
)
|
)
|
||||||
) : (
|
) : null}
|
||||||
<em>{argName}</em>
|
|
||||||
)}
|
|
||||||
{showShortcuts && (
|
{showShortcuts && (
|
||||||
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
||||||
<span className="sr-only">Hotkey: </span>
|
<span className="sr-only">Hotkey: </span>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
{arg.inputType === 'kcl' &&
|
||||||
|
!!argumentsToSubmit[argName] &&
|
||||||
|
'variableName' in
|
||||||
|
(argumentsToSubmit[argName] as KclCommandValue) && (
|
||||||
|
<>
|
||||||
|
<CustomIcon name="make-variable" className="w-4 h-4" />
|
||||||
|
<Tooltip position="blockEnd">
|
||||||
|
New variable:{' '}
|
||||||
|
{
|
||||||
|
(
|
||||||
|
argumentsToSubmit[
|
||||||
|
argName
|
||||||
|
] as KclExpressionWithVariable
|
||||||
|
).variableName
|
||||||
|
}
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
17
src/components/CommandBar/CommandBarKclInput.module.css
Normal file
17
src/components/CommandBar/CommandBarKclInput.module.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.editor {
|
||||||
|
@apply text-base flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor :global(.cm-editor) {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor :global(.cm-line)::selection {
|
||||||
|
@apply px-1;
|
||||||
|
@apply text-chalkboard-100;
|
||||||
|
@apply bg-energy-10/50;
|
||||||
|
}
|
||||||
|
:global(.dark) .editor :global(.cm-line)::selection {
|
||||||
|
@apply text-energy-10;
|
||||||
|
@apply bg-energy-10/20;
|
||||||
|
}
|
221
src/components/CommandBar/CommandBarKclInput.tsx
Normal file
221
src/components/CommandBar/CommandBarKclInput.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { Completion } from '@codemirror/autocomplete'
|
||||||
|
import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror'
|
||||||
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||||
|
import { getSystemTheme } from 'lib/theme'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
import { roundOff } from 'lib/utils'
|
||||||
|
import { varMentions } from 'lib/varCompletionExtension'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import styles from './CommandBarKclInput.module.css'
|
||||||
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
|
|
||||||
|
function CommandBarKclInput({
|
||||||
|
arg,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
arg: CommandArgument<unknown> & {
|
||||||
|
inputType: 'kcl'
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (event: unknown) => void
|
||||||
|
}) {
|
||||||
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
|
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||||
|
arg.name
|
||||||
|
] as KclCommandValue | undefined
|
||||||
|
const { settings } = useGlobalStateContext()
|
||||||
|
const defaultValue = (arg.defaultValue as string) || ''
|
||||||
|
const [value, setValue] = useState(
|
||||||
|
previouslySetValue?.valueText || defaultValue || ''
|
||||||
|
)
|
||||||
|
const [createNewVariable, setCreateNewVariable] = useState(
|
||||||
|
previouslySetValue && 'variableName' in previouslySetValue
|
||||||
|
)
|
||||||
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
prevVariables,
|
||||||
|
calcResult,
|
||||||
|
newVariableInsertIndex,
|
||||||
|
valueNode,
|
||||||
|
newVariableName,
|
||||||
|
setNewVariableName,
|
||||||
|
isNewVariableNameUnique,
|
||||||
|
} = useCalculateKclExpression({
|
||||||
|
value,
|
||||||
|
initialVariableName:
|
||||||
|
previouslySetValue && 'variableName' in previouslySetValue
|
||||||
|
? previouslySetValue.variableName
|
||||||
|
: arg.name,
|
||||||
|
})
|
||||||
|
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||||
|
label: v.key,
|
||||||
|
detail: String(roundOff(v.value as number)),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { setContainer } = useCodeMirror({
|
||||||
|
container: editorRef.current,
|
||||||
|
value,
|
||||||
|
indentWithTab: false,
|
||||||
|
basicSetup: false,
|
||||||
|
autoFocus: true,
|
||||||
|
selection: {
|
||||||
|
anchor: 0,
|
||||||
|
head:
|
||||||
|
previouslySetValue && 'valueText' in previouslySetValue
|
||||||
|
? previouslySetValue.valueText.length
|
||||||
|
: defaultValue.length,
|
||||||
|
},
|
||||||
|
accessKey: 'command-bar',
|
||||||
|
theme:
|
||||||
|
settings.context.theme === 'system'
|
||||||
|
? getSystemTheme()
|
||||||
|
: settings.context.theme,
|
||||||
|
extensions: [
|
||||||
|
EditorView.domEventHandlers({
|
||||||
|
keydown: (event) => {
|
||||||
|
if (event.key === 'Backspace' && value === '') {
|
||||||
|
event.preventDefault()
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
varMentions(varMentionData),
|
||||||
|
EditorState.transactionFilter.of((tr) => {
|
||||||
|
if (tr.newDoc.lines > 1) {
|
||||||
|
handleSubmit()
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return tr
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onChange: (newValue) => setValue(newValue),
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
setContainer(editorRef.current)
|
||||||
|
}
|
||||||
|
}, [arg, editorRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCanSubmit(
|
||||||
|
calcResult !== 'NAN' && (!createNewVariable || isNewVariableNameUnique)
|
||||||
|
)
|
||||||
|
}, [calcResult, createNewVariable, isNewVariableNameUnique])
|
||||||
|
|
||||||
|
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e?.preventDefault()
|
||||||
|
if (!canSubmit || valueNode === null) return
|
||||||
|
|
||||||
|
onSubmit(
|
||||||
|
createNewVariable
|
||||||
|
? ({
|
||||||
|
valueAst: valueNode,
|
||||||
|
valueText: value,
|
||||||
|
valueCalculated: calcResult,
|
||||||
|
variableName: newVariableName,
|
||||||
|
insertIndex: newVariableInsertIndex,
|
||||||
|
variableIdentifierAst: createIdentifier(newVariableName),
|
||||||
|
variableDeclarationAst: createVariableDeclaration(
|
||||||
|
newVariableName,
|
||||||
|
valueNode
|
||||||
|
),
|
||||||
|
} satisfies KclCommandValue)
|
||||||
|
: ({
|
||||||
|
valueAst: valueNode,
|
||||||
|
valueText: value,
|
||||||
|
valueCalculated: calcResult,
|
||||||
|
} satisfies KclCommandValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="arg-form" onSubmit={handleSubmit} data-can-submit={canSubmit}>
|
||||||
|
<label className="flex gap-4 items-center mx-4 my-4 border-solid border-b border-chalkboard-50">
|
||||||
|
<span className="capitalize text-chalkboard-80 dark:text-chalkboard-20">
|
||||||
|
{arg.name}
|
||||||
|
</span>
|
||||||
|
<div ref={editorRef} className={styles.editor} />
|
||||||
|
<CustomIcon
|
||||||
|
name="equal"
|
||||||
|
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
calcResult === 'NAN'
|
||||||
|
? 'text-destroy-80 dark:text-destroy-40'
|
||||||
|
: 'text-energy-60 dark:text-energy-20'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{calcResult === 'NAN'
|
||||||
|
? "Can't calculate"
|
||||||
|
: roundOff(Number(calcResult), 4)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{createNewVariable ? (
|
||||||
|
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
|
||||||
|
<label
|
||||||
|
htmlFor="variable-name"
|
||||||
|
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
|
||||||
|
>
|
||||||
|
Variable name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="variable-name"
|
||||||
|
name="variable-name"
|
||||||
|
className="flex-1 border-none bg-transparent"
|
||||||
|
placeholder="Variable name"
|
||||||
|
value={newVariableName}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck="false"
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setNewVariableName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.currentTarget.value === '' && e.key === 'Backspace') {
|
||||||
|
setCreateNewVariable(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
isNewVariableNameUnique
|
||||||
|
? 'text-energy-60 dark:text-energy-20'
|
||||||
|
: 'text-destroy-60 dark:text-destroy-40'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isNewVariableNameUnique ? 'Available' : 'Unavailable'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-between gap-2 px-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setCreateNewVariable(true)}
|
||||||
|
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
|
||||||
|
>
|
||||||
|
<CustomIcon name="plus" className="w-5 h-5" />
|
||||||
|
Create new variable
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarKclInput
|
@ -14,7 +14,18 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
|
[
|
||||||
|
'alt+1',
|
||||||
|
'alt+2',
|
||||||
|
'alt+3',
|
||||||
|
'alt+4',
|
||||||
|
'alt+5',
|
||||||
|
'alt+6',
|
||||||
|
'alt+7',
|
||||||
|
'alt+8',
|
||||||
|
'alt+9',
|
||||||
|
'alt+0',
|
||||||
|
],
|
||||||
(_, b) => {
|
(_, b) => {
|
||||||
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
||||||
if (!selectedCommand?.args) return
|
if (!selectedCommand?.args) return
|
||||||
|
@ -18,10 +18,12 @@ export type CustomIconName =
|
|||||||
| 'horizontal'
|
| 'horizontal'
|
||||||
| 'horizontalDash'
|
| 'horizontalDash'
|
||||||
| 'line'
|
| 'line'
|
||||||
|
| 'make-variable'
|
||||||
| 'move'
|
| 'move'
|
||||||
| 'network'
|
| 'network'
|
||||||
| 'networkCrossedOut'
|
| 'networkCrossedOut'
|
||||||
| 'parallel'
|
| 'parallel'
|
||||||
|
| 'plus'
|
||||||
| 'search'
|
| 'search'
|
||||||
| 'settings'
|
| 'settings'
|
||||||
| 'sketch'
|
| 'sketch'
|
||||||
@ -336,6 +338,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'make-variable':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4.07178 6.57735L9.99998 3.1547L15.9282 6.57735V13.4227L9.99998 16.8453L4.07178 13.4227V6.57735ZM9.99998 2L16.9282 6V14L9.99998 18L3.07178 14V6L9.99998 2ZM9.45068 6.854C9.20802 6.798 8.97468 6.78867 8.75068 6.826C8.39602 6.90067 8.06468 7.04533 7.75668 7.26C7.73802 7.26933 7.72402 7.27867 7.71468 7.288C7.45335 7.484 7.24802 7.694 7.09868 7.918C6.96802 8.09533 6.86068 8.282 6.77668 8.478C6.69268 8.65533 6.63668 8.814 6.60868 8.954C6.60868 9.00067 6.62268 9.038 6.65068 9.066L6.69268 9.108H6.95868C7.13602 9.108 7.23402 9.09867 7.25268 9.08C7.28068 9.052 7.30868 8.982 7.33668 8.87C7.45802 8.52467 7.65402 8.212 7.92468 7.932C8.13002 7.72667 8.36802 7.58667 8.63868 7.512C8.83468 7.456 9.02602 7.456 9.21268 7.512C9.40868 7.57733 9.53002 7.68467 9.57668 7.834C9.62335 7.96467 9.61402 8.198 9.54868 8.534L8.77868 11.614C8.65735 11.9593 8.47535 12.216 8.23268 12.384C8.10202 12.4587 7.97602 12.4913 7.85468 12.482C7.68668 12.482 7.53735 12.4307 7.40668 12.328L7.36468 12.286L7.42068 12.272C7.50468 12.244 7.57002 12.216 7.61668 12.188C7.93402 12.02 8.10668 11.7493 8.13468 11.376C8.15335 11.1053 8.05535 10.9187 7.84068 10.816C7.60735 10.6853 7.34135 10.69 7.04268 10.83C6.73468 10.9793 6.54802 11.2547 6.48268 11.656C6.45468 11.8893 6.47335 12.1087 6.53868 12.314C6.56668 12.4073 6.60868 12.4913 6.66468 12.566C6.92602 12.986 7.32268 13.182 7.85468 13.154C8.31202 13.126 8.72268 12.8787 9.08668 12.412L9.12868 12.37L9.21268 12.496C9.44602 12.8133 9.80068 13.0233 10.2767 13.126C10.5474 13.1633 10.79 13.1633 11.0047 13.126C11.6954 12.9767 12.2507 12.58 12.6707 11.936C12.6894 11.9173 12.7034 11.894 12.7127 11.866C12.9553 11.474 13.0767 11.18 13.0767 10.984C13.0767 10.9373 13.0674 10.9047 13.0487 10.886C13.0207 10.8673 12.918 10.858 12.7407 10.858C12.61 10.858 12.526 10.8627 12.4887 10.872C12.442 10.8813 12.4047 10.9327 12.3767 11.026C12.2834 11.3807 12.092 11.7073 11.8027 12.006C11.56 12.23 11.3174 12.3793 11.0747 12.454C11.0094 12.4727 10.9067 12.482 10.7667 12.482C10.6174 12.482 10.5194 12.4727 10.4727 12.454C10.314 12.398 10.1974 12.3 10.1227 12.16C10.0667 12.0573 10.062 11.8613 10.1087 11.572C10.1087 11.5347 10.132 11.4367 10.1787 11.278C10.58 9.542 10.8274 8.55733 10.9207 8.324C11.0887 7.88533 11.3127 7.61467 11.5927 7.512C11.6114 7.50267 11.63 7.498 11.6487 7.498C11.8914 7.43267 12.0967 7.47467 12.2647 7.624L12.3207 7.68L12.2087 7.722C11.8354 7.85267 11.6207 8.128 11.5647 8.548C11.5367 8.76267 11.5927 8.94 11.7327 9.08C11.77 9.11733 11.8167 9.15 11.8727 9.178C12.1714 9.32733 12.4887 9.28067 12.8247 9.038C12.9367 8.954 13.03 8.83267 13.1047 8.674C13.282 8.26333 13.2774 7.87133 13.0907 7.498C12.9787 7.26467 12.7874 7.078 12.5167 6.938C12.162 6.77933 11.8074 6.76533 11.4527 6.896C11.1447 7.01733 10.8787 7.20867 10.6547 7.47L10.5707 7.582C10.552 7.582 10.524 7.554 10.4867 7.498C10.2627 7.17133 9.91735 6.95667 9.45068 6.854Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'move':
|
case 'move':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -400,6 +418,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'plus':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'search':
|
case 'search':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -13,7 +13,7 @@ type OutputTypeKey = OutputFormat['type']
|
|||||||
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
||||||
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
||||||
|
|
||||||
interface ExportButtonProps extends React.PropsWithChildren {
|
export interface ExportButtonProps extends React.PropsWithChildren {
|
||||||
className?: {
|
className?: {
|
||||||
button?: string
|
button?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
|
@ -3,8 +3,9 @@ import { BrowserRouter } from 'react-router-dom'
|
|||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar/CommandBar'
|
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { ExportButtonProps } from './ExportButton'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -37,15 +38,22 @@ const projectWellFormed = {
|
|||||||
},
|
},
|
||||||
} satisfies ProjectWithEntryPointMetadata
|
} satisfies ProjectWithEntryPointMetadata
|
||||||
|
|
||||||
|
const mockExportButton = vi.fn()
|
||||||
|
vi.mock('/src/components/ExportButton', () => ({
|
||||||
|
// engineCommandManager method call in ExportButton causes vitest to hang
|
||||||
|
ExportButton: (props: ExportButtonProps) => {
|
||||||
|
mockExportButton(props)
|
||||||
|
return <button>Fake export button</button>
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
describe('ProjectSidebarMenu tests', () => {
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
|
||||||
<GlobalStateProvider>
|
<GlobalStateProvider>
|
||||||
<ProjectSidebarMenu project={projectWellFormed} />
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
</GlobalStateProvider>
|
</GlobalStateProvider>
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -62,11 +70,9 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders app name if given no project', () => {
|
test('Renders app name if given no project', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
|
||||||
<GlobalStateProvider>
|
<GlobalStateProvider>
|
||||||
<ProjectSidebarMenu />
|
<ProjectSidebarMenu />
|
||||||
</GlobalStateProvider>
|
</GlobalStateProvider>
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,14 +84,9 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders as a link if set to do so', () => {
|
test('Renders as a link if set to do so', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
|
||||||
<GlobalStateProvider>
|
<GlobalStateProvider>
|
||||||
<ProjectSidebarMenu
|
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
|
||||||
project={projectWellFormed}
|
|
||||||
renderAsLink={true}
|
|
||||||
/>
|
|
||||||
</GlobalStateProvider>
|
</GlobalStateProvider>
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
|
|||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
useCalc,
|
|
||||||
CalcResult,
|
CalcResult,
|
||||||
CreateNewVariable,
|
CreateNewVariable,
|
||||||
} from './AvailableVarsHelpers'
|
} from './AvailableVarsHelpers'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
|
||||||
type ModalResolve = {
|
type ModalResolve = {
|
||||||
value: string
|
value: string
|
||||||
@ -55,7 +55,7 @@ export const SetAngleLengthModal = ({
|
|||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
inputRef,
|
inputRef,
|
||||||
newVariableInsertIndex,
|
newVariableInsertIndex,
|
||||||
} = useCalc({
|
} = useCalculateKclExpression({
|
||||||
value,
|
value,
|
||||||
initialVariableName: valueName,
|
initialVariableName: valueName,
|
||||||
})
|
})
|
||||||
|
@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
|
|||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
useCalc,
|
|
||||||
CalcResult,
|
CalcResult,
|
||||||
CreateNewVariable,
|
CreateNewVariable,
|
||||||
} from './AvailableVarsHelpers'
|
} from './AvailableVarsHelpers'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
|
||||||
type ModalResolve = {
|
type ModalResolve = {
|
||||||
value: string
|
value: string
|
||||||
@ -59,7 +59,7 @@ export const GetInfoModal = ({
|
|||||||
newVariableName,
|
newVariableName,
|
||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
newVariableInsertIndex,
|
newVariableInsertIndex,
|
||||||
} = useCalc({ value: value, initialVariableName })
|
} = useCalculateKclExpression({ value: value, initialVariableName })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
import { CreateNewVariable } from './AvailableVarsHelpers'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { type InstanceProps, create } from 'react-modal-promise'
|
import { type InstanceProps, create } from 'react-modal-promise'
|
||||||
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
|
|
||||||
type ModalResolve = { variableName: string }
|
type ModalResolve = { variableName: string }
|
||||||
type ModalReject = boolean
|
type ModalReject = boolean
|
||||||
@ -25,7 +26,7 @@ export const SetVarNameModal = ({
|
|||||||
valueName,
|
valueName,
|
||||||
}: SetVarNameModalProps) => {
|
}: SetVarNameModalProps) => {
|
||||||
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
||||||
useCalc({ value: '', initialVariableName: valueName })
|
useCalculateKclExpression({ value: '', initialVariableName: valueName })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
@ -25,6 +25,7 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import interact from '@replit/codemirror-interact'
|
import interact from '@replit/codemirror-interact'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import { useFileContext } from 'hooks/useFileContext'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||||
@ -85,6 +86,9 @@ export const TextEditor = ({
|
|||||||
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
||||||
useGlobalStateContext()
|
useGlobalStateContext()
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const {
|
||||||
|
context: { project },
|
||||||
|
} = useFileContext()
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
|
|
||||||
@ -107,7 +111,7 @@ export const TextEditor = ({
|
|||||||
}, [setIsKclLspServerReady])
|
}, [setIsKclLspServerReady])
|
||||||
|
|
||||||
// Here we initialize the plugin which will start the client.
|
// Here we initialize the plugin which will start the client.
|
||||||
// When we have multi-file support the name of the file will be a dep of
|
// Now that we have multi-file support the name of the file is a dep of
|
||||||
// this use memo, as well as the directory structure, which I think is
|
// this use memo, as well as the directory structure, which I think is
|
||||||
// a good setup because it will restart the client but not the server :)
|
// a good setup because it will restart the client but not the server :)
|
||||||
// We do not want to restart the server, its just wasteful.
|
// We do not want to restart the server, its just wasteful.
|
||||||
@ -163,7 +167,7 @@ export const TextEditor = ({
|
|||||||
plugin = lsp
|
plugin = lsp
|
||||||
}
|
}
|
||||||
return plugin
|
return plugin
|
||||||
}, [copilotLspClient, isCopilotLspServerReady])
|
}, [copilotLspClient, isCopilotLspServerReady, project])
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
const onChange = (newCode: string) => {
|
const onChange = (newCode: string) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { parse, recast, initPromise } from './wasm'
|
import { parse, recast, initPromise, Identifier } from './wasm'
|
||||||
import {
|
import {
|
||||||
createLiteral,
|
createLiteral,
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
@ -90,7 +90,17 @@ describe('Testing createPipeExpression', () => {
|
|||||||
describe('Testing findUniqueName', () => {
|
describe('Testing findUniqueName', () => {
|
||||||
it('should find a unique name', () => {
|
it('should find a unique name', () => {
|
||||||
const result = findUniqueName(
|
const result = findUniqueName(
|
||||||
'yo01 yo02 yo03 yo04 yo05 yo06 yo07 yo08 yo09',
|
JSON.stringify([
|
||||||
|
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
|
||||||
|
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
|
||||||
|
] satisfies Identifier[]),
|
||||||
'yo',
|
'yo',
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
@ -162,18 +162,32 @@ export function findUniqueName(
|
|||||||
pad = 3,
|
pad = 3,
|
||||||
index = 1
|
index = 1
|
||||||
): string {
|
): string {
|
||||||
let searchStr = ''
|
let searchStr: string = typeof ast === 'string' ? ast : JSON.stringify(ast)
|
||||||
if (typeof ast === 'string') {
|
const indexStr = String(index).padStart(pad, '0')
|
||||||
searchStr = ast
|
|
||||||
} else {
|
const endingDigitsMatcher = /\d+$/
|
||||||
searchStr = JSON.stringify(ast)
|
const nameEndsInDigits = name.match(endingDigitsMatcher)
|
||||||
|
let nameIsInString = searchStr.includes(`:"${name}"`)
|
||||||
|
|
||||||
|
if (nameEndsInDigits !== null) {
|
||||||
|
// base case: name is unique and ends in digits
|
||||||
|
if (!nameIsInString) return name
|
||||||
|
|
||||||
|
// recursive case: name is not unique and ends in digits
|
||||||
|
const newPad = nameEndsInDigits[1].length
|
||||||
|
const newIndex = parseInt(nameEndsInDigits[1]) + 1
|
||||||
|
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
||||||
|
|
||||||
|
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
||||||
}
|
}
|
||||||
const indexStr = `${index}`.padStart(pad, '0')
|
|
||||||
const newName = `${name}${indexStr}`
|
const newName = `${name}${indexStr}`
|
||||||
const isInString = searchStr.includes(newName)
|
nameIsInString = searchStr.includes(`:"${newName}"`)
|
||||||
if (!isInString) {
|
|
||||||
return newName
|
// base case: name is unique and does not end in digits
|
||||||
}
|
if (!nameIsInString) return newName
|
||||||
|
|
||||||
|
// recursive case: name is not unique and does not end in digits
|
||||||
return findUniqueName(searchStr, name, pad, index + 1)
|
return findUniqueName(searchStr, name, pad, index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +287,7 @@ export function extrudeSketch(
|
|||||||
node: Program,
|
node: Program,
|
||||||
pathToNode: PathToNode,
|
pathToNode: PathToNode,
|
||||||
shouldPipe = true,
|
shouldPipe = true,
|
||||||
distance = 4
|
distance = createLiteral(4) as Value
|
||||||
): {
|
): {
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
@ -299,7 +313,7 @@ export function extrudeSketch(
|
|||||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||||
|
|
||||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||||
createLiteral(distance),
|
distance,
|
||||||
shouldPipe
|
shouldPipe
|
||||||
? createPipeSubstitution()
|
? createPipeSubstitution()
|
||||||
: {
|
: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CommandSetConfig } from 'lib/commandTypes'
|
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export type ModelingCommandSchema = {
|
|||||||
Extrude: {
|
Extrude: {
|
||||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
distance: number
|
distance: KclCommandValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,8 +50,8 @@ export const modelingMachineConfig: CommandSetConfig<
|
|||||||
// })),
|
// })),
|
||||||
// },
|
// },
|
||||||
distance: {
|
distance: {
|
||||||
inputType: 'number',
|
inputType: 'kcl',
|
||||||
defaultValue: 5,
|
defaultValue: '5 + 7',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -7,10 +7,23 @@ import {
|
|||||||
InterpreterFrom,
|
InterpreterFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { Selection } from './selections'
|
import { Selection } from './selections'
|
||||||
|
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
||||||
|
|
||||||
type Icon = CustomIconName
|
type Icon = CustomIconName
|
||||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
|
const INPUT_TYPES = ['options', 'string', 'kcl', 'selection'] as const
|
||||||
|
export interface KclExpression {
|
||||||
|
valueAst: Value
|
||||||
|
valueText: string
|
||||||
|
valueCalculated: string
|
||||||
|
}
|
||||||
|
export interface KclExpressionWithVariable extends KclExpression {
|
||||||
|
variableName: string
|
||||||
|
variableDeclarationAst: VariableDeclaration
|
||||||
|
variableIdentifierAst: Identifier
|
||||||
|
insertIndex: number
|
||||||
|
}
|
||||||
|
export type KclCommandValue = KclExpression | KclExpressionWithVariable
|
||||||
export type CommandInputType = (typeof INPUT_TYPES)[number]
|
export type CommandInputType = (typeof INPUT_TYPES)[number]
|
||||||
|
|
||||||
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
|
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
|
||||||
@ -82,20 +95,24 @@ export type CommandArgumentConfig<
|
|||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required: boolean
|
||||||
skip?: true
|
skip?: true
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options:
|
options:
|
||||||
| CommandArgumentOption<OutputType>[]
|
| CommandArgumentOption<OutputType>[]
|
||||||
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
||||||
|
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
selectionTypes: Selection['type'][]
|
selectionTypes: Selection['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
}
|
}
|
||||||
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
|
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type CommandArgument<
|
export type CommandArgument<
|
||||||
@ -106,11 +123,11 @@ export type CommandArgument<
|
|||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required: boolean
|
||||||
skip?: true
|
skip?: true
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options: CommandArgumentOption<OutputType>[]
|
options: CommandArgumentOption<OutputType>[]
|
||||||
|
defaultValue?: OutputType
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
@ -118,7 +135,11 @@ export type CommandArgument<
|
|||||||
actor: InterpreterFrom<T>
|
actor: InterpreterFrom<T>
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
}
|
}
|
||||||
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
|
defaultValue?: OutputType
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type CommandArgumentWithName<
|
export type CommandArgumentWithName<
|
||||||
|
15
src/lib/commandUtils.ts
Normal file
15
src/lib/commandUtils.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Some command argument payloads are objects with a value field that is a KCL expression.
|
||||||
|
// That object also contains some metadata about what to do with the KCL expression,
|
||||||
|
// such as whether we need to create a new variable for it.
|
||||||
|
// This function extracts the value field from those arg payloads and returns
|
||||||
|
// The arg object with all its field as natural values that the command to be executed will expect.
|
||||||
|
export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(args).map(([key, value]) => {
|
||||||
|
if (value !== null && typeof value === 'object' && 'value' in value) {
|
||||||
|
return [key, value.value]
|
||||||
|
}
|
||||||
|
return [key, value]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
@ -97,7 +97,7 @@ function buildCommandArguments<
|
|||||||
|
|
||||||
for (const arg in args) {
|
for (const arg in args) {
|
||||||
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||||
const newArg = buildCommandArgument(argConfig, state, actor)
|
const newArg = buildCommandArgument(argConfig, arg, state, actor)
|
||||||
newArgs[arg] = newArg
|
newArgs[arg] = newArg
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +109,7 @@ function buildCommandArgument<
|
|||||||
T extends AnyStateMachine
|
T extends AnyStateMachine
|
||||||
>(
|
>(
|
||||||
arg: CommandArgumentConfig<O, T>,
|
arg: CommandArgumentConfig<O, T>,
|
||||||
|
argName: string,
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
actor?: InterpreterFrom<T>
|
actor?: InterpreterFrom<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
@ -116,10 +117,6 @@ function buildCommandArgument<
|
|||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
skip: arg.skip,
|
skip: arg.skip,
|
||||||
defaultValue:
|
|
||||||
arg.defaultValue instanceof Function
|
|
||||||
? arg.defaultValue(state.context)
|
|
||||||
: arg.defaultValue,
|
|
||||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||||
|
|
||||||
if (arg.inputType === 'options') {
|
if (arg.inputType === 'options') {
|
||||||
@ -136,6 +133,10 @@ function buildCommandArgument<
|
|||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
|
defaultValue:
|
||||||
|
arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(state.context)
|
||||||
|
: arg.defaultValue,
|
||||||
options,
|
options,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||||
} else if (arg.inputType === 'selection') {
|
} else if (arg.inputType === 'selection') {
|
||||||
@ -149,9 +150,19 @@ function buildCommandArgument<
|
|||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
actor,
|
actor,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
|
} else if (arg.inputType === 'kcl') {
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
defaultValue: arg.defaultValue,
|
||||||
|
...baseCommandArgument,
|
||||||
|
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
|
defaultValue:
|
||||||
|
arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(state.context)
|
||||||
|
: arg.defaultValue,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export function isTauri(): boolean {
|
export function isTauri(): boolean {
|
||||||
if (typeof window !== 'undefined') {
|
if (globalThis.window && typeof globalThis.window !== 'undefined') {
|
||||||
return '__TAURI__' in window
|
return '__TAURI__' in globalThis.window
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
126
src/lib/useCalculateKclExpression.ts
Normal file
126
src/lib/useCalculateKclExpression.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import { findUniqueName } from 'lang/modifyAst'
|
||||||
|
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
|
||||||
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
|
import { Value, parse } from 'lang/wasm'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { executeAst } from 'useStore'
|
||||||
|
|
||||||
|
const isValidVariableName = (name: string) =>
|
||||||
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a value and a possible variablename,
|
||||||
|
* return helpers for calculating the value and inserting it into the code
|
||||||
|
* as well as information about the variables that are available
|
||||||
|
*/
|
||||||
|
export function useCalculateKclExpression({
|
||||||
|
value,
|
||||||
|
initialVariableName: valueName = '',
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
initialVariableName?: string
|
||||||
|
}): {
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>
|
||||||
|
valueNode: Value | null
|
||||||
|
calcResult: string
|
||||||
|
prevVariables: PrevVariable<unknown>[]
|
||||||
|
newVariableName: string
|
||||||
|
isNewVariableNameUnique: boolean
|
||||||
|
newVariableInsertIndex: number
|
||||||
|
setNewVariableName: (a: string) => void
|
||||||
|
} {
|
||||||
|
const { programMemory } = useKclContext()
|
||||||
|
const { context } = useModelingContext()
|
||||||
|
const selectionRange = context.selectionRanges.codeBasedSelections[0].range
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [availableVarInfo, setAvailableVarInfo] = useState<
|
||||||
|
ReturnType<typeof findAllPreviousVariables>
|
||||||
|
>({
|
||||||
|
variables: [],
|
||||||
|
insertIndex: 0,
|
||||||
|
bodyPath: [],
|
||||||
|
})
|
||||||
|
const [valueNode, setValueNode] = useState<Value | null>(null)
|
||||||
|
const [calcResult, setCalcResult] = useState('NAN')
|
||||||
|
const [newVariableName, setNewVariableName] = useState('')
|
||||||
|
const [isNewVariableNameUnique, setIsNewVariableNameUnique] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current && inputRef.current.focus()
|
||||||
|
inputRef.current &&
|
||||||
|
inputRef.current.setSelectionRange(0, String(value).length)
|
||||||
|
}, 100)
|
||||||
|
setNewVariableName(findUniqueName(kclManager.ast, valueName))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allVarNames = Object.keys(programMemory.root)
|
||||||
|
if (
|
||||||
|
allVarNames.includes(newVariableName) ||
|
||||||
|
newVariableName === '' ||
|
||||||
|
!isValidVariableName(newVariableName)
|
||||||
|
) {
|
||||||
|
setIsNewVariableNameUnique(false)
|
||||||
|
} else {
|
||||||
|
setIsNewVariableNameUnique(true)
|
||||||
|
}
|
||||||
|
}, [newVariableName])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!programMemory || !selectionRange) return
|
||||||
|
const varInfo = findAllPreviousVariables(
|
||||||
|
kclManager.ast,
|
||||||
|
kclManager.programMemory,
|
||||||
|
selectionRange
|
||||||
|
)
|
||||||
|
setAvailableVarInfo(varInfo)
|
||||||
|
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const execAstAndSetResult = async () => {
|
||||||
|
const code = `const __result__ = ${value}`
|
||||||
|
const ast = parse(code)
|
||||||
|
const _programMem: any = { root: {}, return: null }
|
||||||
|
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||||
|
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||||
|
})
|
||||||
|
const { programMemory } = await executeAst({
|
||||||
|
ast,
|
||||||
|
engineCommandManager,
|
||||||
|
useFakeExecutor: true,
|
||||||
|
programMemoryOverride: JSON.parse(
|
||||||
|
JSON.stringify(kclManager.programMemory)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
const resultDeclaration = ast.body.find(
|
||||||
|
(a) =>
|
||||||
|
a.type === 'VariableDeclaration' &&
|
||||||
|
a.declarations?.[0]?.id?.name === '__result__'
|
||||||
|
)
|
||||||
|
const init =
|
||||||
|
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||||
|
resultDeclaration?.declarations?.[0]?.init
|
||||||
|
const result = programMemory?.root?.__result__?.value
|
||||||
|
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||||
|
init && setValueNode(init)
|
||||||
|
}
|
||||||
|
execAstAndSetResult().catch(() => {
|
||||||
|
setCalcResult('NAN')
|
||||||
|
setValueNode(null)
|
||||||
|
})
|
||||||
|
}, [value, availableVarInfo])
|
||||||
|
|
||||||
|
return {
|
||||||
|
valueNode,
|
||||||
|
calcResult,
|
||||||
|
prevVariables: availableVarInfo.variables,
|
||||||
|
newVariableInsertIndex: availableVarInfo.insertIndex,
|
||||||
|
newVariableName,
|
||||||
|
isNewVariableNameUnique,
|
||||||
|
setNewVariableName,
|
||||||
|
inputRef,
|
||||||
|
}
|
||||||
|
}
|
23
src/lib/usePreviousVarMentions.ts
Normal file
23
src/lib/usePreviousVarMentions.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { CompletionContext } from '@codemirror/autocomplete'
|
||||||
|
import { usePreviousVariables } from './usePreviousVariables'
|
||||||
|
|
||||||
|
/// Basically a fork of the `mentions` extension https://github.com/uiwjs/react-codemirror/blob/master/extensions/mentions/src/index.ts
|
||||||
|
/// But it matches on any word, not just the `@` symbol
|
||||||
|
export function usePreviousVarMentions(context: CompletionContext) {
|
||||||
|
const previousVariables = usePreviousVariables()
|
||||||
|
const data = previousVariables.variables.map((variable) => {
|
||||||
|
return {
|
||||||
|
label: variable.key,
|
||||||
|
detail: variable.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let word = context.matchBefore(/^\w*$/)
|
||||||
|
if (!word) return null
|
||||||
|
if (word && word.from === word.to && !context.explicit) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
from: word?.from!,
|
||||||
|
options: [...data],
|
||||||
|
}
|
||||||
|
}
|
30
src/lib/usePreviousVariables.ts
Normal file
30
src/lib/usePreviousVariables.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import { findAllPreviousVariables } from 'lang/queryAst'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function usePreviousVariables() {
|
||||||
|
const { programMemory, code } = useKclContext()
|
||||||
|
const { context } = useModelingContext()
|
||||||
|
const selectionRange = context.selectionRanges.codeBasedSelections[0]
|
||||||
|
?.range || [code.length, code.length]
|
||||||
|
const [previousVariablesInfo, setPreviousVariablesInfo] = useState<
|
||||||
|
ReturnType<typeof findAllPreviousVariables>
|
||||||
|
>({
|
||||||
|
variables: [],
|
||||||
|
insertIndex: 0,
|
||||||
|
bodyPath: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!programMemory || !selectionRange) return
|
||||||
|
const varInfo = findAllPreviousVariables(
|
||||||
|
kclManager.ast,
|
||||||
|
kclManager.programMemory,
|
||||||
|
selectionRange
|
||||||
|
)
|
||||||
|
setPreviousVariablesInfo(varInfo)
|
||||||
|
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||||
|
|
||||||
|
return previousVariablesInfo
|
||||||
|
}
|
28
src/lib/varCompletionExtension.ts
Normal file
28
src/lib/varCompletionExtension.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Extension } from '@codemirror/state'
|
||||||
|
import {
|
||||||
|
CompletionContext,
|
||||||
|
autocompletion,
|
||||||
|
Completion,
|
||||||
|
} from '@codemirror/autocomplete'
|
||||||
|
|
||||||
|
/// Basically a fork of the `mentions` extension https://github.com/uiwjs/react-codemirror/blob/master/extensions/mentions/src/index.ts
|
||||||
|
/// But it matches on any word, not just the `@` symbol
|
||||||
|
export function varMentions(data: Completion[] = []): Extension {
|
||||||
|
return autocompletion({
|
||||||
|
override: [
|
||||||
|
(context: CompletionContext) => {
|
||||||
|
let word = context.matchBefore(/(\w+)?/)
|
||||||
|
if (!word) return null
|
||||||
|
if (word && word.from === word.to && !context.explicit) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
from: word?.from!,
|
||||||
|
options: [...data],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const varMentionsView: Extension = [varMentions()]
|
@ -3,12 +3,14 @@ import {
|
|||||||
Command,
|
Command,
|
||||||
CommandArgument,
|
CommandArgument,
|
||||||
CommandArgumentWithName,
|
CommandArgumentWithName,
|
||||||
|
KclCommandValue,
|
||||||
} from 'lib/commandTypes'
|
} from 'lib/commandTypes'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
|
|
||||||
export const commandBarMachine = createMachine(
|
export const commandBarMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSkWB6coHI16ByVBACBt8oHFAtHma0qR6iemlKsLGHppg61cJWDY+wzRqEDrGJs35IZ4AIV5eGswGabG9FJPqdRQo0NUqiBhmESjpFZrghcjYPiQB4bTEEU8NbImyFiaQ1hCw0M2Eja8mEVBmn5KCQOSVdL6SvvAISw87qzDPDKBQ-IdB8npkWH0FoJR8lcOKKeWgHAWNmslaxEcjKmXMmtSM1lbIqJHvuQi2x-KuPMPsQKb1HaBT6roWwqlNDBOYlYyWi1loX2ifEu6lQpDWlUNaHRsxUjSCFBsGY1TdB2i5BkBQgdhYmUqfbTI4JPr9lUHIdIxFCi6M-h6bBWcMz6xrjWbe7xG6Rj7pHAZ3lUgSLcD6NIApFj7jEVMrICkbA0VLjyHWd4vBAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSjHFPHQNFCIzk3jWRURJIAQNvlA4oFo8zWlSPUT00pVhYw9NMHWrhKwbH2GaNQgdYxNm-JDPAxCvLw1mAzTY3opJ9TqKFehqlUTMMwiUdIrNA5gMbB8IhQlh5bkWFsVwyJshYmkNYQs9DNhI2vJhFQZp+SgkDklXS+kr7wHUZA+G-UzwygUPyLB+xApFh9BaCU6hpSLGqNKDheC5yBlsfNCORlTLmTWpGaytl+G0wwhoEU7ilDWnMN4zGhRPqO0Cn1XQthVKaBsbNZK9iYlLUyhfBJKSR7OH2FYAi1oDGzFSNIIUGwZiVD0BKTCSkFCB2FiZRpIlMjgk+v2VQch0jEUKIYz+HoOSaA0IeJRO8m4OImXLTQjDYRpAFIsfckillZAUjYGipceQ6zvF4IAA */
|
||||||
context: {
|
context: {
|
||||||
commands: [] as Command[],
|
commands: [] as Command[],
|
||||||
selectedCommand: undefined as Command | undefined,
|
selectedCommand: undefined as Command | undefined,
|
||||||
@ -143,7 +145,7 @@ export const commandBarMachine = createMachine(
|
|||||||
'Change current argument': {
|
'Change current argument': {
|
||||||
target: 'Gathering arguments',
|
target: 'Gathering arguments',
|
||||||
internal: true,
|
internal: true,
|
||||||
actions: ['Set current argument'],
|
actions: ['Remove current argument and set a new one'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'Deselect command': {
|
'Deselect command': {
|
||||||
@ -172,17 +174,7 @@ export const commandBarMachine = createMachine(
|
|||||||
|
|
||||||
'Remove argument': {
|
'Remove argument': {
|
||||||
target: 'Review',
|
target: 'Review',
|
||||||
actions: [
|
actions: ['Remove argument'],
|
||||||
assign({
|
|
||||||
argumentsToSubmit: (context, event) => {
|
|
||||||
const argName = Object.keys(event.data)[0]
|
|
||||||
const { argumentsToSubmit } = context
|
|
||||||
const newArgumentsToSubmit = { ...argumentsToSubmit }
|
|
||||||
newArgumentsToSubmit[argName] = undefined
|
|
||||||
return newArgumentsToSubmit
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'Edit argument': {
|
'Edit argument': {
|
||||||
@ -272,7 +264,7 @@ export const commandBarMachine = createMachine(
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'Change current argument'
|
type: 'Change current argument'
|
||||||
data: { arg: CommandArgumentWithName<unknown> }
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
predictableActionArguments: true,
|
predictableActionArguments: true,
|
||||||
@ -283,19 +275,17 @@ export const commandBarMachine = createMachine(
|
|||||||
'Execute command': (context, event) => {
|
'Execute command': (context, event) => {
|
||||||
const { selectedCommand } = context
|
const { selectedCommand } = context
|
||||||
if (!selectedCommand) return
|
if (!selectedCommand) return
|
||||||
if (selectedCommand?.args) {
|
if (
|
||||||
selectedCommand?.onSubmit(
|
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||||
event.type === 'Submit command' ||
|
|
||||||
event.type === 'done.invoke.validateArguments'
|
event.type === 'done.invoke.validateArguments'
|
||||||
? event.data
|
) {
|
||||||
: undefined
|
selectedCommand?.onSubmit(getCommandArgumentKclValuesOnly(event.data))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
selectedCommand?.onSubmit()
|
selectedCommand?.onSubmit()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Set current argument to first non-skippable': assign({
|
'Set current argument to first non-skippable': assign({
|
||||||
currentArgument: (context, event) => {
|
currentArgument: (context) => {
|
||||||
const { selectedCommand } = context
|
const { selectedCommand } = context
|
||||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||||
|
|
||||||
@ -331,6 +321,15 @@ export const commandBarMachine = createMachine(
|
|||||||
'Clear current argument': assign({
|
'Clear current argument': assign({
|
||||||
currentArgument: undefined,
|
currentArgument: undefined,
|
||||||
}),
|
}),
|
||||||
|
'Remove argument': assign({
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
if (event.type !== 'Remove argument') return context.argumentsToSubmit
|
||||||
|
const argToRemove = Object.values(event.data)[0]
|
||||||
|
// Extract all but the argument to remove and return it
|
||||||
|
const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
}),
|
||||||
'Set current argument': assign({
|
'Set current argument': assign({
|
||||||
currentArgument: (context, event) => {
|
currentArgument: (context, event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@ -338,13 +337,34 @@ export const commandBarMachine = createMachine(
|
|||||||
return event.data.arg
|
return event.data.arg
|
||||||
case 'Edit argument':
|
case 'Edit argument':
|
||||||
return event.data.arg
|
return event.data.arg
|
||||||
case 'Change current argument':
|
|
||||||
return event.data.arg
|
|
||||||
default:
|
default:
|
||||||
return context.currentArgument
|
return context.currentArgument
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
'Remove current argument and set a new one': assign({
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
if (event.type !== 'Change current argument')
|
||||||
|
return context.currentArgument
|
||||||
|
return Object.values(event.data)[0]
|
||||||
|
},
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
if (
|
||||||
|
event.type !== 'Change current argument' ||
|
||||||
|
!context.currentArgument
|
||||||
|
)
|
||||||
|
return context.argumentsToSubmit
|
||||||
|
const { name, required } = context.currentArgument
|
||||||
|
if (required)
|
||||||
|
return {
|
||||||
|
[name]: undefined,
|
||||||
|
...context.argumentsToSubmit,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { [name]: _, ...rest } = context.argumentsToSubmit
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
}),
|
||||||
'Clear argument data': assign({
|
'Clear argument data': assign({
|
||||||
selectedCommand: undefined,
|
selectedCommand: undefined,
|
||||||
currentArgument: undefined,
|
currentArgument: undefined,
|
||||||
@ -378,7 +398,8 @@ export const commandBarMachine = createMachine(
|
|||||||
if (!command.args) return {}
|
if (!command.args) return {}
|
||||||
const args: { [x: string]: unknown } = {}
|
const args: { [x: string]: unknown } = {}
|
||||||
for (const [argName, arg] of Object.entries(command.args)) {
|
for (const [argName, arg] of Object.entries(command.args)) {
|
||||||
args[argName] = arg.skip ? arg.defaultValue : undefined
|
args[argName] =
|
||||||
|
arg.skip && 'defaultValue' in arg ? arg.defaultValue : undefined
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
},
|
},
|
||||||
@ -406,8 +427,12 @@ export const commandBarMachine = createMachine(
|
|||||||
let argConfig = context.selectedCommand!.args![argName]
|
let argConfig = context.selectedCommand!.args![argName]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(argConfig.defaultValue &&
|
('defaultValue' in argConfig &&
|
||||||
typeof arg !== typeof argConfig.defaultValue) ||
|
argConfig.defaultValue &&
|
||||||
|
typeof arg !== typeof argConfig.defaultValue &&
|
||||||
|
argConfig.inputType !== 'kcl') ||
|
||||||
|
(argConfig.inputType === 'kcl' &&
|
||||||
|
!(arg as Partial<KclCommandValue>).valueAst) ||
|
||||||
('options' in argConfig &&
|
('options' in argConfig &&
|
||||||
typeof arg !== typeof argConfig.options[0].value)
|
typeof arg !== typeof argConfig.options[0].value)
|
||||||
) {
|
) {
|
||||||
|
@ -732,15 +732,32 @@ export const modelingMachine = createMachine(
|
|||||||
'AST extrude': (_, event) => {
|
'AST extrude': (_, event) => {
|
||||||
if (!event.data) return
|
if (!event.data) return
|
||||||
const { selection, distance } = event.data
|
const { selection, distance } = event.data
|
||||||
|
let ast = kclManager.ast
|
||||||
|
if (
|
||||||
|
'variableName' in distance &&
|
||||||
|
distance.variableName &&
|
||||||
|
distance.insertIndex !== undefined
|
||||||
|
) {
|
||||||
|
console.log('adding variable!', distance)
|
||||||
|
const newBody = [...ast.body]
|
||||||
|
newBody.splice(
|
||||||
|
distance.insertIndex,
|
||||||
|
0,
|
||||||
|
distance.variableDeclarationAst
|
||||||
|
)
|
||||||
|
ast.body = newBody
|
||||||
|
}
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
kclManager.ast,
|
ast,
|
||||||
selection.codeBasedSelections[0].range
|
selection.codeBasedSelections[0].range
|
||||||
)
|
)
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
kclManager.ast,
|
ast,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
true,
|
true,
|
||||||
distance
|
'variableName' in distance
|
||||||
|
? distance.variableIdentifierAst
|
||||||
|
: distance.valueAst
|
||||||
)
|
)
|
||||||
// TODO not handling focusPath correctly I think
|
// TODO not handling focusPath correctly I think
|
||||||
kclManager.updateAst(modifiedAst, true, {
|
kclManager.updateAst(modifiedAst, true, {
|
||||||
|
Reference in New Issue
Block a user