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:
@ -57,12 +57,11 @@ export const CommandBarProvider = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CommandBar />
|
||||
</CommandsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandBar = () => {
|
||||
export const CommandBar = () => {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const {
|
||||
context: { selectedCommand, currentArgument, commands },
|
||||
@ -84,17 +83,25 @@ const CommandBar = () => {
|
||||
if (commandBarState.matches('Review')) {
|
||||
const entries = Object.entries(selectedCommand?.args || {})
|
||||
|
||||
commandBarSend({
|
||||
type: commandBarState.matches('Review')
|
||||
? 'Edit argument'
|
||||
: 'Change current argument',
|
||||
data: {
|
||||
arg: {
|
||||
name: entries[entries.length - 1][0],
|
||||
...entries[entries.length - 1][1],
|
||||
const currentArgName = entries[entries.length - 1][0]
|
||||
const currentArg = {
|
||||
name: currentArgName,
|
||||
...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 {
|
||||
commandBarSend({ type: 'Deselect command' })
|
||||
}
|
||||
@ -117,6 +124,11 @@ const CommandBar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => console.log(commandBarState.context.argumentsToSubmit),
|
||||
[commandBarState.context.argumentsToSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={!commandBarState.matches('Closed') || false}
|
||||
|
@ -4,6 +4,7 @@ import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import CommandBarHeader from './CommandBarHeader'
|
||||
import CommandBarKclInput from './CommandBarKclInput'
|
||||
|
||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
@ -17,10 +18,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||
commandBarSend({
|
||||
type: 'Submit argument',
|
||||
data: {
|
||||
[currentArgument.name]:
|
||||
currentArgument.inputType === 'number'
|
||||
? parseFloat((data as string) || '0')
|
||||
: data,
|
||||
[currentArgument.name]: data,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -68,6 +66,10 @@ function ArgumentInput({
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
case 'kcl':
|
||||
return (
|
||||
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<CommandBarBasicInput
|
||||
|
@ -9,7 +9,7 @@ function CommandBarBasicInput({
|
||||
onSubmit,
|
||||
}: {
|
||||
arg: CommandArgument<unknown> & {
|
||||
inputType: 'number' | 'string'
|
||||
inputType: 'string'
|
||||
name: string
|
||||
}
|
||||
stepBack: () => void
|
||||
@ -18,7 +18,6 @@ function CommandBarBasicInput({
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const inputType = arg.inputType === 'number' ? 'number' : 'text'
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
@ -40,9 +39,9 @@ function CommandBarBasicInput({
|
||||
</span>
|
||||
<input
|
||||
id="arg-form"
|
||||
name={inputType}
|
||||
name={arg.inputType}
|
||||
ref={inputRef}
|
||||
type={inputType}
|
||||
type={arg.inputType}
|
||||
required
|
||||
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"
|
||||
|
@ -4,6 +4,9 @@ import React, { ReactNode, useState } from 'react'
|
||||
import { ActionButton } from '../ActionButton'
|
||||
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
||||
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<{}>) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
@ -45,6 +48,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
parseInt(b.keys[0], 10) - 1
|
||||
]
|
||||
const arg = selectedCommand?.args[argName]
|
||||
if (!argName || !arg) return
|
||||
commandBarSend({
|
||||
type: 'Change current argument',
|
||||
data: { arg: { ...arg, name: argName } },
|
||||
@ -59,7 +63,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
selectedCommand &&
|
||||
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">
|
||||
<p
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<span className="capitalize">{argName}</span>
|
||||
{argumentsToSubmit[argName] ? (
|
||||
arg.inputType === 'selection' ? (
|
||||
getSelectionTypeDisplayText(
|
||||
argumentsToSubmit[argName] as Selections
|
||||
)
|
||||
) : arg.inputType === 'kcl' ? (
|
||||
roundOff(
|
||||
Number(
|
||||
(argumentsToSubmit[argName] as KclCommandValue)
|
||||
.valueCalculated
|
||||
),
|
||||
4
|
||||
)
|
||||
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
||||
JSON.stringify(argumentsToSubmit[argName])
|
||||
) : (
|
||||
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
||||
)
|
||||
) : (
|
||||
<em>{argName}</em>
|
||||
)}
|
||||
) : null}
|
||||
{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">
|
||||
<span className="sr-only">Hotkey: </span>
|
||||
{i + 1}
|
||||
</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>
|
||||
)
|
||||
)}
|
||||
|
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(
|
||||
'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) => {
|
||||
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
||||
if (!selectedCommand?.args) return
|
||||
|
Reference in New Issue
Block a user