Replace values with variable helper (#84)
* Refactor getNodePathFromSourceRange getNodePathFromSourceRange wouldn't go as deep as it should have, stopping at pipe expressions, when it should have followed as deep into the ast as possible. The fact that it stopped early then had other part of the code base that expected this behaviour and it effected a lot, so a rather large refactor * overhaul of getNodePathFromSourceRange * quick fix for moreNodePathFromSourceRange * minor bugs in moreNodePathFromSourceRange * couple more tests * add moveValueIntoNewVariable * add UI for replacing valuse with variable * update button text
This commit is contained in:
@ -8,6 +8,7 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||
import { Intersect } from './components/Toolbar/Intersect'
|
||||
import { SetHorzDistance } from './components/Toolbar/SetHorzDistance'
|
||||
import { SetAngleLength } from './components/Toolbar/SetAngleLength'
|
||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const {
|
||||
@ -162,6 +163,7 @@ export const Toolbar = () => {
|
||||
)
|
||||
})}
|
||||
<br></br>
|
||||
<ConvertToVariable />
|
||||
<HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
|
@ -188,12 +188,14 @@ export const CreateNewVariable = ({
|
||||
setNewVariableName,
|
||||
shouldCreateVariable,
|
||||
setShouldCreateVariable,
|
||||
showCheckbox = true,
|
||||
}: {
|
||||
isNewVariableNameUnique: boolean
|
||||
newVariableName: string
|
||||
setNewVariableName: (a: string) => void
|
||||
shouldCreateVariable: boolean
|
||||
setShouldCreateVariable: (a: boolean) => void
|
||||
showCheckbox?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@ -204,6 +206,7 @@ export const CreateNewVariable = ({
|
||||
Create new variable
|
||||
</label>
|
||||
<div className="mt-1 flex flex-1">
|
||||
{showCheckbox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
|
||||
@ -212,6 +215,7 @@ export const CreateNewVariable = ({
|
||||
setShouldCreateVariable(e.target.checked)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
disabled={!shouldCreateVariable}
|
||||
|
86
src/components/SetVarNameModal.tsx
Normal file
86
src/components/SetVarNameModal.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
||||
|
||||
export const SetVarNameModal = ({
|
||||
isOpen,
|
||||
onResolve,
|
||||
onReject,
|
||||
valueName,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onResolve: (a: { variableName?: string }) => void
|
||||
onReject: (a: any) => void
|
||||
value: number
|
||||
valueName: string
|
||||
}) => {
|
||||
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
||||
useCalc({ value: '', initialVariableName: valueName })
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onReject}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 capitalize"
|
||||
>
|
||||
Set {valueName}
|
||||
</Dialog.Title>
|
||||
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
setShouldCreateVariable={() => {}}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||
!isNewVariableNameUnique
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
61
src/components/Toolbar/ConvertVariable.tsx
Normal file
61
src/components/Toolbar/ConvertVariable.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { useStore } from '../../useStore'
|
||||
import { isNodeSafeToReplace } from '../../lang/queryAst'
|
||||
import { SetVarNameModal } from '../SetVarNameModal'
|
||||
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
|
||||
|
||||
const getModalInfo = create(SetVarNameModal as any)
|
||||
|
||||
export const ConvertToVariable = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||
(s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
})
|
||||
)
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
|
||||
const { isSafe, value } = isNodeSafeToReplace(ast, selectionRanges[0])
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection = selectionRanges.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnableAngLen(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!ast) return
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
} as any)
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
selectionRanges[0],
|
||||
variableName
|
||||
)
|
||||
|
||||
updateAst(_modifiedAst)
|
||||
} catch (e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
}}
|
||||
className={`border m-1 px-1 rounded text-xs ${
|
||||
enableAngLen ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
|
||||
}`}
|
||||
disabled={!enableAngLen}
|
||||
>
|
||||
ConvertToVariable
|
||||
</button>
|
||||
)
|
||||
}
|
@ -34,7 +34,7 @@ export const SetAngleLength = ({
|
||||
programMemory: s.programMemory,
|
||||
})
|
||||
)
|
||||
const [enableHorz, setEnableHorz] = useState(false)
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
@ -42,7 +42,8 @@ export const SetAngleLength = ({
|
||||
getNodePathFromSourceRange(ast, selectionRange)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression').node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
@ -54,7 +55,7 @@ export const SetAngleLength = ({
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||
setEnableHorz(_enableHorz)
|
||||
setEnableAngLen(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
@ -102,9 +103,9 @@ export const SetAngleLength = ({
|
||||
}
|
||||
}}
|
||||
className={`border m-1 px-1 rounded text-xs ${
|
||||
enableHorz ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
|
||||
enableAngLen ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
|
||||
}`}
|
||||
disabled={!enableHorz}
|
||||
disabled={!enableAngLen}
|
||||
>
|
||||
{angleOrLength}
|
||||
</button>
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { PathToNode } from './executor'
|
||||
import { Token } from './tokeniser'
|
||||
import { parseExpression } from './astMathExpressions'
|
||||
import { Range } from '../useStore'
|
||||
|
||||
type syntaxType =
|
||||
export type SyntaxType =
|
||||
| 'Program'
|
||||
| 'ExpressionStatement'
|
||||
| 'BinaryExpression'
|
||||
@ -81,14 +79,14 @@ type syntaxType =
|
||||
// | 'TypeAnnotation'
|
||||
|
||||
export interface Program {
|
||||
type: syntaxType
|
||||
type: SyntaxType
|
||||
start: number
|
||||
end: number
|
||||
body: BodyItem[]
|
||||
nonCodeMeta: NoneCodeMeta
|
||||
}
|
||||
interface GeneralStatement {
|
||||
type: syntaxType
|
||||
type: SyntaxType
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
@ -1102,7 +1100,7 @@ function makeBlockStatement(
|
||||
}
|
||||
}
|
||||
|
||||
interface ReturnStatement extends GeneralStatement {
|
||||
export interface ReturnStatement extends GeneralStatement {
|
||||
type: 'ReturnStatement'
|
||||
argument: Value
|
||||
}
|
||||
|
@ -229,7 +229,13 @@ show(mySketch)
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: ['body', 0, 'declarations', 0, 'init'],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
@ -239,11 +245,23 @@ show(mySketch)
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: ['body', 1, 'declarations', 0, 'init'],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [27, 49],
|
||||
},
|
||||
{
|
||||
pathToNode: ['body', 0, 'declarations', 0, 'init'],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
@ -258,15 +276,16 @@ show(mySketch)
|
||||
const { root } = exe(code)
|
||||
expect(root.yo).toEqual({
|
||||
type: 'userVal',
|
||||
value: {
|
||||
aStr: 'str',
|
||||
anum: 2,
|
||||
identifier: 3,
|
||||
binExp: 9,
|
||||
},
|
||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: ['body', 1, 'declarations', 0, 'init'],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [27, 83],
|
||||
},
|
||||
],
|
||||
@ -282,7 +301,13 @@ show(mySketch)
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: ['body', 1, 'declarations', 0, 'init'],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [41, 50],
|
||||
},
|
||||
],
|
||||
|
@ -15,7 +15,7 @@ import { internalFns } from './std/std'
|
||||
import { BufferGeometry } from 'three'
|
||||
|
||||
export type SourceRange = [number, number]
|
||||
export type PathToNode = (string | number)[]
|
||||
export type PathToNode = [string | number, string][]
|
||||
export type Metadata = {
|
||||
sourceRange: SourceRange
|
||||
pathToNode: PathToNode
|
||||
@ -130,13 +130,13 @@ export const executor = (
|
||||
if (statement.type === 'VariableDeclaration') {
|
||||
statement.declarations.forEach((declaration, index) => {
|
||||
const variableName = declaration.id.name
|
||||
const pathToNode = [
|
||||
const pathToNode: PathToNode = [
|
||||
...previousPathToNode,
|
||||
'body',
|
||||
bodyIndex,
|
||||
'declarations',
|
||||
index,
|
||||
'init',
|
||||
['body', ''],
|
||||
[bodyIndex, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[index, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
]
|
||||
const sourceRange: SourceRange = [
|
||||
declaration.init.start,
|
||||
|
@ -11,10 +11,12 @@ import {
|
||||
findUniqueName,
|
||||
addSketchTo,
|
||||
giveSketchFnCallTag,
|
||||
moveValueIntoNewVariable,
|
||||
} from './modifyAst'
|
||||
import { recast } from './recast'
|
||||
import { lexer } from './tokeniser'
|
||||
import { initPromise } from './rust'
|
||||
import { executor } from './executor'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
|
||||
@ -173,3 +175,93 @@ show(part001)`
|
||||
expect(isTagExisting).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing moveValueIntoNewVariable', () => {
|
||||
const fn = (fnName: string) => `const ${fnName} = (x) => {
|
||||
return x
|
||||
}
|
||||
`
|
||||
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
|
||||
const abc = 3
|
||||
const identifierGuy = 5
|
||||
const part001 = startSketchAt([-1.2, 4.83])
|
||||
|> line([2.8, 0], %)
|
||||
|> angledLine([100 + 100, 3.09], %)
|
||||
|> angledLine([abc, 3.09], %)
|
||||
|> angledLine([def('yo'), 3.09], %)
|
||||
|> angledLine([ghi(%), 3.09], %)
|
||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
||||
const yo = 5 + 6
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)`
|
||||
it('should move a value into a new variable', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const programMemory = executor(ast)
|
||||
const startIndex = code.indexOf('100 + 100') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = 100 + 100`)
|
||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||
})
|
||||
it('should move a value into a new variable', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const programMemory = executor(ast)
|
||||
const startIndex = code.indexOf('2.8') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = 2.8`)
|
||||
expect(newCode).toContain(`line([newVar, 0], %)`)
|
||||
})
|
||||
it('should move a value into a new variable', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const programMemory = executor(ast)
|
||||
const startIndex = code.indexOf('def(')
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = def('yo')`)
|
||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||
})
|
||||
it('should move a value into a new variable', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const programMemory = executor(ast)
|
||||
const startIndex = code.indexOf('jkl(') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = jkl('yo') + 2`)
|
||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||
})
|
||||
it('should move a value into a new variable', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const programMemory = executor(ast)
|
||||
const startIndex = code.indexOf('identifierGuy +') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = identifierGuy + 5`)
|
||||
expect(newCode).toContain(`const yo2 = hmm([newVar])`)
|
||||
})
|
||||
})
|
||||
|
@ -15,7 +15,12 @@ import {
|
||||
UnaryExpression,
|
||||
BinaryExpression,
|
||||
} from './abstractSyntaxTree'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from './queryAst'
|
||||
import {
|
||||
findAllPreviousVariables,
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
isNodeSafeToReplace,
|
||||
} from './queryAst'
|
||||
import { PathToNode, ProgramMemory } from './executor'
|
||||
import {
|
||||
addTagForSketchOnFace,
|
||||
@ -27,7 +32,7 @@ export function addSketchTo(
|
||||
node: Program,
|
||||
axis: 'xy' | 'xz' | 'yz',
|
||||
name = ''
|
||||
): { modifiedAst: Program; id: string; pathToNode: (string | number)[] } {
|
||||
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
||||
const _node = { ...node }
|
||||
const _name = name || findUniqueName(node, 'part')
|
||||
|
||||
@ -63,15 +68,15 @@ export function addSketchTo(
|
||||
newBody.splice(showCallIndex, 0, variableDeclaration)
|
||||
_node.body = newBody
|
||||
}
|
||||
let pathToNode: (string | number)[] = [
|
||||
'body',
|
||||
sketchIndex,
|
||||
'declarations',
|
||||
'0',
|
||||
'init',
|
||||
let pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndex, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
]
|
||||
if (axis !== 'xy') {
|
||||
pathToNode = [...pathToNode, 'body', '0']
|
||||
pathToNode = [...pathToNode, ['body', ''], ['0', 'index']]
|
||||
}
|
||||
|
||||
return {
|
||||
@ -194,7 +199,7 @@ export function mutateObjExpProp(
|
||||
|
||||
export function extrudeSketch(
|
||||
node: Program,
|
||||
pathToNode: (string | number)[],
|
||||
pathToNode: PathToNode,
|
||||
shouldPipe = true
|
||||
): {
|
||||
modifiedAst: Program
|
||||
@ -217,7 +222,7 @@ export function extrudeSketch(
|
||||
)
|
||||
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
||||
|
||||
const { node: variableDeclorator, path: pathToDecleration } =
|
||||
const { node: variableDeclorator, shallowPath: pathToDecleration } =
|
||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||
|
||||
const extrudeCall = createCallExpression('extrude', [
|
||||
@ -239,13 +244,13 @@ export function extrudeSketch(
|
||||
)
|
||||
|
||||
variableDeclorator.init = pipeChain
|
||||
const pathToExtrudeArg = [
|
||||
const pathToExtrudeArg: PathToNode = [
|
||||
...pathToDecleration,
|
||||
'init',
|
||||
'body',
|
||||
pipeChain.body.length - 1,
|
||||
'arguments',
|
||||
0,
|
||||
['init', 'VariableDeclarator'],
|
||||
['body', ''],
|
||||
[pipeChain.body.length - 1, 'index'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
return {
|
||||
@ -258,30 +263,30 @@ export function extrudeSketch(
|
||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||
const showCallIndex = getShowIndex(_node)
|
||||
_node.body.splice(showCallIndex, 0, VariableDeclaration)
|
||||
const pathToExtrudeArg = [
|
||||
'body',
|
||||
showCallIndex,
|
||||
'declarations',
|
||||
0,
|
||||
'init',
|
||||
'arguments',
|
||||
0,
|
||||
const pathToExtrudeArg: PathToNode = [
|
||||
['body', ''],
|
||||
[showCallIndex, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
return {
|
||||
modifiedAst: addToShow(_node, name),
|
||||
pathToNode: [...pathToNode.slice(0, -1), showCallIndex],
|
||||
pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']],
|
||||
pathToExtrudeArg,
|
||||
}
|
||||
}
|
||||
|
||||
export function sketchOnExtrudedFace(
|
||||
node: Program,
|
||||
pathToNode: (string | number)[],
|
||||
pathToNode: PathToNode,
|
||||
programMemory: ProgramMemory
|
||||
): { modifiedAst: Program; pathToNode: (string | number)[] } {
|
||||
): { modifiedAst: Program; pathToNode: PathToNode } {
|
||||
let _node = { ...node }
|
||||
const newSketchName = findUniqueName(node, 'part')
|
||||
const { node: oldSketchNode, path: pathToOldSketch } =
|
||||
const { node: oldSketchNode, shallowPath: pathToOldSketch } =
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
@ -330,7 +335,7 @@ export function sketchOnExtrudedFace(
|
||||
|
||||
return {
|
||||
modifiedAst: addToShow(_node, newSketchName),
|
||||
pathToNode: [...pathToNode.slice(0, -1), expressionIndex],
|
||||
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,10 +347,10 @@ export function splitPathAtLastIndex(pathToNode: PathToNode): {
|
||||
index: number
|
||||
} {
|
||||
const last = pathToNode[pathToNode.length - 1]
|
||||
if (typeof last === 'number') {
|
||||
if (last && typeof last[0] === 'number') {
|
||||
return {
|
||||
path: pathToNode.slice(0, -1),
|
||||
index: last,
|
||||
index: last[0],
|
||||
}
|
||||
} else if (pathToNode.length === 0) {
|
||||
return {
|
||||
@ -356,6 +361,32 @@ export function splitPathAtLastIndex(pathToNode: PathToNode): {
|
||||
return splitPathAtLastIndex(pathToNode.slice(0, -1))
|
||||
}
|
||||
|
||||
export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
||||
path: PathToNode
|
||||
index: number
|
||||
} {
|
||||
const last = pathToNode[pathToNode.length - 1]
|
||||
|
||||
if (
|
||||
last &&
|
||||
last[1] === 'index' &&
|
||||
pathToNode?.[pathToNode.length - 2]?.[1] === 'PipeExpression' &&
|
||||
typeof last[0] === 'number'
|
||||
) {
|
||||
return {
|
||||
path: pathToNode.slice(0, -1),
|
||||
index: last[0],
|
||||
}
|
||||
} else if (pathToNode.length === 0) {
|
||||
return {
|
||||
path: [],
|
||||
index: -1,
|
||||
}
|
||||
}
|
||||
|
||||
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
||||
}
|
||||
|
||||
export function createLiteral(value: string | number): Literal {
|
||||
return {
|
||||
type: 'Literal',
|
||||
@ -499,7 +530,8 @@ export function giveSketchFnCallTag(
|
||||
): { modifiedAst: Program; tag: string; isTagExisting: boolean } {
|
||||
const { node: primaryCallExp } = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
getNodePathFromSourceRange(ast, range),
|
||||
'CallExpression'
|
||||
)
|
||||
const firstArg = getFirstArg(primaryCallExp)
|
||||
const isTagExisting = !!firstArg.tag
|
||||
@ -518,3 +550,29 @@ export function giveSketchFnCallTag(
|
||||
isTagExisting,
|
||||
}
|
||||
}
|
||||
|
||||
export function moveValueIntoNewVariable(
|
||||
ast: Program,
|
||||
programMemory: ProgramMemory,
|
||||
sourceRange: Range,
|
||||
variableName: string
|
||||
): {
|
||||
modifiedAst: Program
|
||||
} {
|
||||
const { isSafe, value, replacer } = isNodeSafeToReplace(ast, sourceRange)
|
||||
if (!isSafe || value.type === 'Identifier') return { modifiedAst: ast }
|
||||
|
||||
const { insertIndex } = findAllPreviousVariables(
|
||||
ast,
|
||||
programMemory,
|
||||
sourceRange
|
||||
)
|
||||
let _node = JSON.parse(JSON.stringify(ast))
|
||||
_node = replacer(_node, variableName).modifiedAst
|
||||
_node.body.splice(
|
||||
insertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, value)
|
||||
)
|
||||
return { modifiedAst: _node }
|
||||
}
|
||||
|
@ -1,8 +1,20 @@
|
||||
import { abstractSyntaxTree } from './abstractSyntaxTree'
|
||||
import { findAllPreviousVariables } from './queryAst'
|
||||
import {
|
||||
findAllPreviousVariables,
|
||||
isNodeSafeToReplace,
|
||||
isTypeInValue,
|
||||
getNodePathFromSourceRange,
|
||||
} from './queryAst'
|
||||
import { lexer } from './tokeniser'
|
||||
import { initPromise } from './rust'
|
||||
import { executor } from './executor'
|
||||
import {
|
||||
createArrayExpression,
|
||||
createCallExpression,
|
||||
createLiteral,
|
||||
createPipeSubstitution,
|
||||
} from './modifyAst'
|
||||
import { recast } from './recast'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
|
||||
@ -43,6 +55,194 @@ show(part001)`
|
||||
// there are 4 number variables and 2 non-number variables before the sketch var
|
||||
// ∴ the insert index should be 6
|
||||
expect(insertIndex).toEqual(6)
|
||||
expect(bodyPath).toEqual(['body'])
|
||||
expect(bodyPath).toEqual([['body', '']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing argIsNotIdentifier', () => {
|
||||
const code = `const part001 = startSketchAt([-1.2, 4.83])
|
||||
|> line([2.8, 0], %)
|
||||
|> angledLine([100 + 100, 3.09], %)
|
||||
|> angledLine([abc, 3.09], %)
|
||||
|> angledLine([def('yo'), 3.09], %)
|
||||
|> angledLine([ghi(%), 3.09], %)
|
||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
||||
const yo = 5 + 6
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)`
|
||||
it('find a safe binaryExpression', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('100 + 100') + 2
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('BinaryExpression')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe('100 + 100')
|
||||
const { modifiedAst } = result.replacer(
|
||||
JSON.parse(JSON.stringify(ast)),
|
||||
'replaceName'
|
||||
)
|
||||
const outCode = recast(modifiedAst)
|
||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||
})
|
||||
it('find a safe Identifier', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('abc')
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('Identifier')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe('abc')
|
||||
})
|
||||
it('find a safe CallExpression', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('def')
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('CallExpression')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe("def('yo')")
|
||||
const { modifiedAst } = result.replacer(
|
||||
JSON.parse(JSON.stringify(ast)),
|
||||
'replaceName'
|
||||
)
|
||||
const outCode = recast(modifiedAst)
|
||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||
})
|
||||
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('ghi')
|
||||
const range: [number, number] = [rangeStart, rangeStart]
|
||||
const result = isNodeSafeToReplace(ast, range)
|
||||
expect(result.isSafe).toBe(false)
|
||||
expect(result.value?.type).toBe('CallExpression')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe('ghi(%)')
|
||||
})
|
||||
it('find an UNsafe Identifier, as it is a callee', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('ine([2.8,')
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||
expect(result.isSafe).toBe(false)
|
||||
expect(result.value?.type).toBe('CallExpression')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe(
|
||||
'line([2.8, 0], %)'
|
||||
)
|
||||
})
|
||||
it("find a safe BinaryExpression that's assigned to a variable", () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('5 + 6') + 1
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('BinaryExpression')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe('5 + 6')
|
||||
const { modifiedAst } = result.replacer(
|
||||
JSON.parse(JSON.stringify(ast)),
|
||||
'replaceName'
|
||||
)
|
||||
const outCode = recast(modifiedAst)
|
||||
expect(outCode).toContain(`const yo = replaceName`)
|
||||
})
|
||||
it('find a safe BinaryExpression that has a CallExpression within', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('jkl') + 1
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('BinaryExpression')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe(
|
||||
"jkl('yo') + 2"
|
||||
)
|
||||
const { modifiedAst } = result.replacer(
|
||||
JSON.parse(JSON.stringify(ast)),
|
||||
'replaceName'
|
||||
)
|
||||
const outCode = recast(modifiedAst)
|
||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||
})
|
||||
it('find a safe BinaryExpression within a CallExpression', () => {
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const rangeStart = code.indexOf('identifierGuy') + 1
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('BinaryExpression')
|
||||
expect(code.slice(result.value.start, result.value.end)).toBe(
|
||||
'identifierGuy + 5'
|
||||
)
|
||||
const { modifiedAst } = result.replacer(
|
||||
JSON.parse(JSON.stringify(ast)),
|
||||
'replaceName'
|
||||
)
|
||||
const outCode = recast(modifiedAst)
|
||||
expect(outCode).toContain(`const yo2 = hmm([replaceName])`)
|
||||
})
|
||||
|
||||
describe('testing isTypeInValue', () => {
|
||||
it('it finds the pipeSubstituion', () => {
|
||||
const val = createCallExpression('yoyo', [
|
||||
createArrayExpression([
|
||||
createLiteral(1),
|
||||
createCallExpression('yoyo2', [createPipeSubstitution()]),
|
||||
createLiteral('hey'),
|
||||
]),
|
||||
])
|
||||
expect(isTypeInValue(val, 'PipeSubstitution')).toBe(true)
|
||||
})
|
||||
it('There is no pipeSubstituion', () => {
|
||||
const val = createCallExpression('yoyo', [
|
||||
createArrayExpression([
|
||||
createLiteral(1),
|
||||
createCallExpression('yoyo2', [createLiteral(5)]),
|
||||
createLiteral('hey'),
|
||||
]),
|
||||
])
|
||||
expect(isTypeInValue(val, 'PipeSubstitution')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing getNodePathFromSourceRange', () => {
|
||||
const code = `const part001 = startSketchAt([0.39, -0.05])
|
||||
|> line([0.94, 2.61], %)
|
||||
|> line([-0.21, -1.4], %)
|
||||
show(part001)`
|
||||
it('it finds the second line when cursor is put at the end', () => {
|
||||
const searchLn = `line([0.94, 2.61], %)`
|
||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['body', 'PipeExpression'],
|
||||
[1, 'index'],
|
||||
])
|
||||
})
|
||||
it('it finds the last line when cursor is put at the end', () => {
|
||||
const searchLn = `line([-0.21, -1.4], %)`
|
||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||
const expected = [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', ''],
|
||||
['body', 'PipeExpression'],
|
||||
[2, 'index'],
|
||||
]
|
||||
expect(result).toEqual(expected)
|
||||
// expect similar result for start of line
|
||||
const startSourceIndex = code.indexOf(searchLn)
|
||||
const startResult = getNodePathFromSourceRange(ast, [
|
||||
startSourceIndex,
|
||||
startSourceIndex,
|
||||
])
|
||||
expect(startResult).toEqual([...expected, ['callee', 'CallExpression']])
|
||||
// expect similar result when whole line is selected
|
||||
const selectWholeThing = getNodePathFromSourceRange(ast, [
|
||||
startSourceIndex,
|
||||
sourceIndex,
|
||||
])
|
||||
expect(selectWholeThing).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
@ -1,16 +1,28 @@
|
||||
import { PathToNode, ProgramMemory } from './executor'
|
||||
import { Range } from '../useStore'
|
||||
import { Program } from './abstractSyntaxTree'
|
||||
import { splitPathAtLastIndex } from './modifyAst'
|
||||
import {
|
||||
BinaryExpression,
|
||||
Program,
|
||||
SyntaxType,
|
||||
Value,
|
||||
CallExpression,
|
||||
ExpressionStatement,
|
||||
VariableDeclaration,
|
||||
ReturnStatement,
|
||||
ArrayExpression,
|
||||
Identifier,
|
||||
} from './abstractSyntaxTree'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
|
||||
export function getNodeFromPath<T>(
|
||||
node: Program,
|
||||
path: (string | number)[],
|
||||
stopAt: string = '',
|
||||
path: PathToNode,
|
||||
stopAt: string | string[] = '',
|
||||
returnEarly = false
|
||||
): {
|
||||
node: T
|
||||
path: PathToNode
|
||||
shallowPath: PathToNode
|
||||
deepPath: PathToNode
|
||||
} {
|
||||
let currentNode = node as any
|
||||
let stopAtNode = null
|
||||
@ -18,21 +30,26 @@ export function getNodeFromPath<T>(
|
||||
let pathsExplored: PathToNode = []
|
||||
for (const pathItem of path) {
|
||||
try {
|
||||
if (typeof currentNode[pathItem] !== 'object')
|
||||
if (typeof currentNode[pathItem[0]] !== 'object')
|
||||
throw new Error('not an object')
|
||||
currentNode = currentNode[pathItem]
|
||||
currentNode = currentNode?.[pathItem[0]]
|
||||
successfulPaths.push(pathItem)
|
||||
if (!stopAtNode) {
|
||||
pathsExplored.push(pathItem)
|
||||
}
|
||||
if (currentNode.type === stopAt) {
|
||||
if (
|
||||
Array.isArray(stopAt)
|
||||
? stopAt.includes(currentNode.type)
|
||||
: currentNode.type === stopAt
|
||||
) {
|
||||
// it will match the deepest node of the type
|
||||
// instead of returning at the first match
|
||||
stopAtNode = currentNode
|
||||
if (returnEarly) {
|
||||
return {
|
||||
node: stopAtNode,
|
||||
path: pathsExplored,
|
||||
shallowPath: pathsExplored,
|
||||
deepPath: successfulPaths,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -48,13 +65,14 @@ export function getNodeFromPath<T>(
|
||||
}
|
||||
return {
|
||||
node: stopAtNode || currentNode,
|
||||
path: pathsExplored,
|
||||
shallowPath: pathsExplored,
|
||||
deepPath: successfulPaths,
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeFromPathCurry(
|
||||
node: Program,
|
||||
path: (string | number)[]
|
||||
path: PathToNode
|
||||
): <T>(
|
||||
stopAt: string,
|
||||
returnEarly?: boolean
|
||||
@ -63,18 +81,171 @@ export function getNodeFromPathCurry(
|
||||
path: PathToNode
|
||||
} {
|
||||
return <T>(stopAt: string = '', returnEarly = false) => {
|
||||
return getNodeFromPath<T>(node, path, stopAt, returnEarly)
|
||||
const { node: _node, shallowPath } = getNodeFromPath<T>(
|
||||
node,
|
||||
path,
|
||||
stopAt,
|
||||
returnEarly
|
||||
)
|
||||
return {
|
||||
node: _node,
|
||||
path: shallowPath,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moreNodePathFromSourceRange(
|
||||
node: Value | ExpressionStatement | VariableDeclaration | ReturnStatement,
|
||||
sourceRange: Range,
|
||||
previousPath: PathToNode = [['body', '']]
|
||||
): PathToNode {
|
||||
const [start, end] = sourceRange
|
||||
let path: PathToNode = [...previousPath]
|
||||
const _node = { ...node }
|
||||
|
||||
if (start < _node.start || end > _node.end) return path
|
||||
|
||||
const isInRange = _node.start <= start && _node.end >= end
|
||||
|
||||
if ((_node.type === 'Identifier' || _node.type === 'Literal') && isInRange)
|
||||
return path
|
||||
|
||||
if (_node.type === 'CallExpression' && isInRange) {
|
||||
const { callee, arguments: args } = _node
|
||||
if (
|
||||
callee.type === 'Identifier' &&
|
||||
callee.start <= start &&
|
||||
callee.end >= end
|
||||
) {
|
||||
path.push(['callee', 'CallExpression'])
|
||||
return path
|
||||
}
|
||||
if (args.length > 0) {
|
||||
for (let argIndex = 0; argIndex < args.length; argIndex++) {
|
||||
const arg = args[argIndex]
|
||||
if (arg.start <= start && arg.end >= end) {
|
||||
path.push(['arguments', 'CallExpression'])
|
||||
path.push([argIndex, 'index'])
|
||||
return moreNodePathFromSourceRange(arg, sourceRange, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
if (_node.type === 'BinaryExpression' && isInRange) {
|
||||
const { left, right } = _node
|
||||
if (left.start <= start && left.end >= end) {
|
||||
path.push(['left', 'BinaryExpression'])
|
||||
return moreNodePathFromSourceRange(left, sourceRange, path)
|
||||
}
|
||||
if (right.start <= start && right.end >= end) {
|
||||
path.push(['right', 'BinaryExpression'])
|
||||
return moreNodePathFromSourceRange(right, sourceRange, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
if (_node.type === 'PipeExpression' && isInRange) {
|
||||
const { body } = _node
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const pipe = body[i]
|
||||
if (pipe.start <= start && pipe.end >= end) {
|
||||
path.push(['body', 'PipeExpression'])
|
||||
path.push([i, 'index'])
|
||||
return moreNodePathFromSourceRange(pipe, sourceRange, path)
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
if (_node.type === 'ArrayExpression' && isInRange) {
|
||||
const { elements } = _node
|
||||
for (let elIndex = 0; elIndex < elements.length; elIndex++) {
|
||||
const element = elements[elIndex]
|
||||
if (element.start <= start && element.end >= end) {
|
||||
path.push(['elements', 'ArrayExpression'])
|
||||
path.push([elIndex, 'index'])
|
||||
return moreNodePathFromSourceRange(element, sourceRange, path)
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
if (_node.type === 'ObjectExpression' && isInRange) {
|
||||
const { properties } = _node
|
||||
for (let propIndex = 0; propIndex < properties.length; propIndex++) {
|
||||
const property = properties[propIndex]
|
||||
if (property.start <= start && property.end >= end) {
|
||||
path.push(['properties', 'ObjectExpression'])
|
||||
path.push([propIndex, 'index'])
|
||||
if (property.key.start <= start && property.key.end >= end) {
|
||||
path.push(['key', 'Property'])
|
||||
return moreNodePathFromSourceRange(property.key, sourceRange, path)
|
||||
}
|
||||
if (property.value.start <= start && property.value.end >= end) {
|
||||
path.push(['value', 'Property'])
|
||||
return moreNodePathFromSourceRange(property.value, sourceRange, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
if (_node.type === 'ExpressionStatement' && isInRange) {
|
||||
const { expression } = _node
|
||||
path.push(['expression', 'ExpressionStatement'])
|
||||
return moreNodePathFromSourceRange(expression, sourceRange, path)
|
||||
}
|
||||
if (_node.type === 'VariableDeclaration' && isInRange) {
|
||||
const declarations = _node.declarations
|
||||
|
||||
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
|
||||
const declaration = declarations[decIndex]
|
||||
if (declaration.start <= start && declaration.end >= end) {
|
||||
path.push(['declarations', 'VariableDeclaration'])
|
||||
path.push([decIndex, 'index'])
|
||||
const init = declaration.init
|
||||
if (init.start <= start && init.end >= end) {
|
||||
path.push(['init', ''])
|
||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_node.type === 'VariableDeclaration' && isInRange) {
|
||||
const declarations = _node.declarations
|
||||
|
||||
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
|
||||
const declaration = declarations[decIndex]
|
||||
if (declaration.start <= start && declaration.end >= end) {
|
||||
const init = declaration.init
|
||||
if (init.start <= start && init.end >= end) {
|
||||
path.push(['declarations', 'VariableDeclaration'])
|
||||
path.push([decIndex, 'index'])
|
||||
path.push(['init', ''])
|
||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
if (_node.type === 'UnaryExpression' && isInRange) {
|
||||
const { argument } = _node
|
||||
if (argument.start <= start && argument.end >= end) {
|
||||
path.push(['argument', 'UnaryExpression'])
|
||||
return moreNodePathFromSourceRange(argument, sourceRange, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
console.error('not implemented')
|
||||
return path
|
||||
}
|
||||
|
||||
export function getNodePathFromSourceRange(
|
||||
node: Program,
|
||||
sourceRange: Range,
|
||||
previousPath: PathToNode = []
|
||||
previousPath: PathToNode = [['body', '']]
|
||||
): PathToNode {
|
||||
const [start, end] = sourceRange
|
||||
let path: PathToNode = [...previousPath, 'body']
|
||||
let path: PathToNode = [...previousPath]
|
||||
const _node = { ...node }
|
||||
|
||||
// loop over each statement in body getting the index with a for loop
|
||||
for (
|
||||
let statementIndex = 0;
|
||||
@ -83,53 +254,8 @@ export function getNodePathFromSourceRange(
|
||||
) {
|
||||
const statement = _node.body[statementIndex]
|
||||
if (statement.start <= start && statement.end >= end) {
|
||||
path.push(statementIndex)
|
||||
if (statement.type === 'ExpressionStatement') {
|
||||
const expression = statement.expression
|
||||
if (expression.start <= start && expression.end >= end) {
|
||||
path.push('expression')
|
||||
if (expression.type === 'CallExpression') {
|
||||
const callee = expression.callee
|
||||
if (callee.start <= start && callee.end >= end) {
|
||||
path.push('callee')
|
||||
if (callee.type === 'Identifier') {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'VariableDeclaration') {
|
||||
const declarations = statement.declarations
|
||||
|
||||
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
|
||||
const declaration = declarations[decIndex]
|
||||
|
||||
if (declaration.start <= start && declaration.end >= end) {
|
||||
path.push('declarations')
|
||||
path.push(decIndex)
|
||||
const init = declaration.init
|
||||
if (init.start <= start && init.end >= end) {
|
||||
path.push('init')
|
||||
if (init.type === 'PipeExpression') {
|
||||
const body = init.body
|
||||
for (let pipeIndex = 0; pipeIndex < body.length; pipeIndex++) {
|
||||
const pipe = body[pipeIndex]
|
||||
if (pipe.start <= start && pipe.end >= end) {
|
||||
path.push('body')
|
||||
path.push(pipeIndex)
|
||||
}
|
||||
}
|
||||
} else if (init.type === 'CallExpression') {
|
||||
const callee = init.callee
|
||||
if (callee.start <= start && callee.end >= end) {
|
||||
path.push('callee')
|
||||
if (callee.type === 'Identifier') {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
path.push([statementIndex, 'index'])
|
||||
return moreNodePathFromSourceRange(statement, sourceRange, path)
|
||||
}
|
||||
}
|
||||
return path
|
||||
@ -151,7 +277,11 @@ export function findAllPreviousVariables(
|
||||
insertIndex: number
|
||||
} {
|
||||
const path = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const { path: pathToDec } = getNodeFromPath(ast, path, 'VariableDeclaration')
|
||||
const { shallowPath: pathToDec } = getNodeFromPath(
|
||||
ast,
|
||||
path,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
const { index: insertIndex, path: bodyPath } = splitPathAtLastIndex(pathToDec)
|
||||
|
||||
const { node: bodyItems } = getNodeFromPath<Program['body']>(ast, bodyPath)
|
||||
@ -174,3 +304,98 @@ export function findAllPreviousVariables(
|
||||
variables,
|
||||
}
|
||||
}
|
||||
|
||||
type ReplacerFn = (_ast: Program, varName: string) => { modifiedAst: Program }
|
||||
|
||||
export function isNodeSafeToReplace(
|
||||
ast: Program,
|
||||
sourceRange: [number, number]
|
||||
): {
|
||||
isSafe: boolean
|
||||
value: Value
|
||||
replacer: ReplacerFn
|
||||
} {
|
||||
let path = getNodePathFromSourceRange(ast, sourceRange)
|
||||
if (path[path.length - 1][0] === 'callee') {
|
||||
path = path.slice(0, -1)
|
||||
}
|
||||
const acceptedNodeTypes = [
|
||||
'BinaryExpression',
|
||||
'Identifier',
|
||||
'CallExpression',
|
||||
'Literal',
|
||||
]
|
||||
const { node: value, deepPath: outPath } = getNodeFromPath(
|
||||
ast,
|
||||
path,
|
||||
acceptedNodeTypes
|
||||
)
|
||||
const { node: binValue, shallowPath: outBinPath } = getNodeFromPath(
|
||||
ast,
|
||||
path,
|
||||
'BinaryExpression'
|
||||
)
|
||||
// binaryExpression should take precedence
|
||||
const [finVal, finPath] =
|
||||
(binValue as Value)?.type === 'BinaryExpression'
|
||||
? [binValue, outBinPath]
|
||||
: [value, outPath]
|
||||
|
||||
const replaceNodeWithIdentifier: ReplacerFn = (_ast, varName) => {
|
||||
const identifier = createIdentifier(varName)
|
||||
const last = finPath[finPath.length - 1]
|
||||
const startPath = finPath.slice(0, -1)
|
||||
const nodeToReplace = getNodeFromPath(_ast, startPath).node as any
|
||||
nodeToReplace[last[0]] = identifier
|
||||
return { modifiedAst: _ast }
|
||||
}
|
||||
|
||||
const hasPipeSub = isTypeInValue(finVal as Value, 'PipeSubstitution')
|
||||
const isIdentifierCallee = path[path.length - 1][0] !== 'callee'
|
||||
return {
|
||||
isSafe:
|
||||
!hasPipeSub &&
|
||||
isIdentifierCallee &&
|
||||
acceptedNodeTypes.includes((finVal as any)?.type) &&
|
||||
finPath.map(([_, type]) => type).includes('VariableDeclaration'),
|
||||
value: finVal as Value,
|
||||
replacer: replaceNodeWithIdentifier,
|
||||
}
|
||||
}
|
||||
|
||||
export function isTypeInValue(node: Value, syntaxType: SyntaxType): boolean {
|
||||
if (node.type === syntaxType) return true
|
||||
if (node.type === 'BinaryExpression') return isTypeInBinExp(node, syntaxType)
|
||||
if (node.type === 'CallExpression') return isTypeInCallExp(node, syntaxType)
|
||||
if (node.type === 'ArrayExpression') return isTypeInArrayExp(node, syntaxType)
|
||||
return false
|
||||
}
|
||||
|
||||
function isTypeInBinExp(
|
||||
node: BinaryExpression,
|
||||
syntaxType: SyntaxType
|
||||
): boolean {
|
||||
if (node.type === syntaxType) return true
|
||||
if (node.left.type === syntaxType) return true
|
||||
if (node.right.type === syntaxType) return true
|
||||
|
||||
return (
|
||||
isTypeInValue(node.left, syntaxType) ||
|
||||
isTypeInValue(node.right, syntaxType)
|
||||
)
|
||||
}
|
||||
|
||||
function isTypeInCallExp(
|
||||
node: CallExpression,
|
||||
syntaxType: SyntaxType
|
||||
): boolean {
|
||||
if (node.callee.type === syntaxType) return true
|
||||
return node.arguments.some((arg) => isTypeInValue(arg, syntaxType))
|
||||
}
|
||||
|
||||
function isTypeInArrayExp(
|
||||
node: ArrayExpression,
|
||||
syntaxType: SyntaxType
|
||||
): boolean {
|
||||
return node.elements.some((el) => isTypeInValue(el, syntaxType))
|
||||
}
|
||||
|
@ -118,7 +118,13 @@ show(mySketch001)`
|
||||
sketchMode: 'sketchEdit',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: ['body', 0, 'declarations', '0', 'init'],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
],
|
||||
},
|
||||
[0, 0]
|
||||
)
|
||||
@ -144,7 +150,13 @@ show(mySketch001)`
|
||||
programMemory,
|
||||
to: [2, 3],
|
||||
fnName: 'lineTo',
|
||||
pathToNode: ['body', 0, 'declarations', '0', 'init'],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
],
|
||||
})
|
||||
const expectedCode = `
|
||||
const mySketch001 = startSketchAt([0, 0])
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
} from '../queryAst'
|
||||
import { lineGeo, sketchBaseGeo } from '../engine'
|
||||
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
||||
import { getLastIndex } from '../modifyAst'
|
||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
||||
|
||||
import {
|
||||
SketchLineHelper,
|
||||
@ -166,7 +166,7 @@ export const lineTo: SketchLineHelper = {
|
||||
createArrayExpression(newVals),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
newVals,
|
||||
@ -292,7 +292,7 @@ export const line: SketchLineHelper = {
|
||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newXVal, newYVal],
|
||||
referencedSegment
|
||||
@ -383,7 +383,7 @@ export const xLineTo: SketchLineHelper = {
|
||||
const newVal = createLiteral(roundOff(to[0], 2))
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([newVal, newVal])
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
@ -450,7 +450,7 @@ export const yLineTo: SketchLineHelper = {
|
||||
const newVal = createLiteral(roundOff(to[1], 2))
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([newVal, newVal])
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
@ -514,7 +514,7 @@ export const xLine: SketchLineHelper = {
|
||||
const firstArg = newVal
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([
|
||||
firstArg,
|
||||
firstArg,
|
||||
@ -577,7 +577,7 @@ export const yLine: SketchLineHelper = {
|
||||
const { node: pipe } = getNode<PipeExpression>('PipeExpression')
|
||||
const newVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
if (replaceExisting && createCallback) {
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback([newVal, newVal])
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
@ -686,7 +686,7 @@ export const angledLine: SketchLineHelper = {
|
||||
])
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newAngleVal, newLengthVal],
|
||||
referencedSegment
|
||||
@ -783,7 +783,7 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
createArrayExpression([angle, xLength]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
if (replaceExisting) {
|
||||
pipe.body[callIndex] = newLine
|
||||
} else {
|
||||
@ -796,7 +796,7 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const { node: callExpression, path } = getNodeFromPath<CallExpression>(
|
||||
const { node: callExpression } = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode
|
||||
)
|
||||
@ -877,7 +877,7 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
createArrayExpression([angle, yLength]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
if (replaceExisting) {
|
||||
pipe.body[callIndex] = newLine
|
||||
} else {
|
||||
@ -890,7 +890,7 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const { node: callExpression, path } = getNodeFromPath<CallExpression>(
|
||||
const { node: callExpression } = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode
|
||||
)
|
||||
@ -957,7 +957,7 @@ export const angledLineToX: SketchLineHelper = {
|
||||
const xArg = createLiteral(roundOff(to[0], 2))
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback([angle, xArg])
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -978,7 +978,7 @@ export const angledLineToX: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const { node: callExpression, path } = getNodeFromPath<CallExpression>(
|
||||
const { node: callExpression } = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode
|
||||
)
|
||||
@ -1043,7 +1043,7 @@ export const angledLineToY: SketchLineHelper = {
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback([angle, yArg])
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -1064,7 +1064,7 @@ export const angledLineToY: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const { node: callExpression, path } = getNodeFromPath<CallExpression>(
|
||||
const { node: callExpression } = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode
|
||||
)
|
||||
@ -1152,7 +1152,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
const { callExp, valueUsedInTransform } = createCallback([angle, offset])
|
||||
const callIndex = getLastIndex(pathToNode)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -1164,7 +1164,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from, previousProgramMemory }) => {
|
||||
const _node = { ...node }
|
||||
const { node: callExpression, path } = getNodeFromPath<CallExpression>(
|
||||
const { node: callExpression } = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode
|
||||
)
|
||||
@ -1234,7 +1234,7 @@ export function changeSketchArguments(
|
||||
): { modifiedAst: Program } {
|
||||
const _node = { ...node }
|
||||
const thePath = getNodePathFromSourceRange(_node, sourceRange)
|
||||
const { node: callExpression, path } = getNodeFromPath<CallExpression>(
|
||||
const { node: callExpression, shallowPath } = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
thePath
|
||||
)
|
||||
@ -1246,7 +1246,7 @@ export function changeSketchArguments(
|
||||
return updateArgs({
|
||||
node: _node,
|
||||
previousProgramMemory: programMemory,
|
||||
pathToNode: path,
|
||||
pathToNode: shallowPath,
|
||||
to: args,
|
||||
from,
|
||||
})
|
||||
@ -1279,11 +1279,8 @@ export function addNewSketchLn({
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const { node: pipeExp, path: pipePath } = getNodeFromPath<PipeExpression>(
|
||||
node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
const { node: pipeExp, shallowPath: pipePath } =
|
||||
getNodeFromPath<PipeExpression>(node, pathToNode, 'PipeExpression')
|
||||
const maybeStartSketchAt = pipeExp.body.find(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
@ -1298,7 +1295,11 @@ export function addNewSketchLn({
|
||||
exp.arguments[0].type === 'Literal' &&
|
||||
exp.arguments[0].value === 'default'
|
||||
)
|
||||
const defaultLinePath = [...pipePath, 'body', maybeDefaultLine]
|
||||
const defaultLinePath: PathToNode = [
|
||||
...pipePath,
|
||||
['body', ''],
|
||||
[maybeDefaultLine, ''],
|
||||
]
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
|
@ -1020,7 +1020,7 @@ export function getRemoveConstraintsTransform(
|
||||
return false
|
||||
}
|
||||
|
||||
export function getTransformMapPath(
|
||||
function getTransformMapPath(
|
||||
sketchFnExp: CallExpression,
|
||||
constraintType: ConstraintType
|
||||
):
|
||||
@ -1137,7 +1137,8 @@ export function getTransformInfos(
|
||||
getNodePathFromSourceRange(ast, selectionRange)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression').node
|
||||
)
|
||||
|
||||
const theTransforms = nodes.map((node) => {
|
||||
@ -1338,7 +1339,8 @@ export function getConstraintLevelFromSourceRange(
|
||||
): 'free' | 'partial' | 'full' {
|
||||
const { node: sketchFnExp } = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
getNodePathFromSourceRange(ast, cursorRange)
|
||||
getNodePathFromSourceRange(ast, cursorRange),
|
||||
'CallExpression'
|
||||
)
|
||||
const name = sketchFnExp?.callee?.name as TooTip
|
||||
if (!toolTips.includes(name)) return 'free'
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ProgramMemory, Path, SourceRange } from '../executor'
|
||||
import { Program, Value } from '../abstractSyntaxTree'
|
||||
import { TooTip } from '../../useStore'
|
||||
import { PathToNode } from '../executor'
|
||||
|
||||
export interface InternalFirstArg {
|
||||
programMemory: ProgramMemory
|
||||
@ -53,7 +54,7 @@ export type InternalFnNames =
|
||||
export interface ModifyAstBase {
|
||||
node: Program
|
||||
previousProgramMemory: ProgramMemory
|
||||
pathToNode: (string | number)[]
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
interface addCall extends ModifyAstBase {
|
||||
@ -85,12 +86,12 @@ export interface SketchLineHelper {
|
||||
fn: InternalFn
|
||||
add: (a: addCall) => {
|
||||
modifiedAst: Program
|
||||
pathToNode: (string | number)[]
|
||||
pathToNode: PathToNode
|
||||
valueUsedInTransform?: number
|
||||
}
|
||||
updateArgs: (a: updateArgs) => {
|
||||
modifiedAst: Program
|
||||
pathToNode: (string | number)[]
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
addTag: (a: ModifyAstBase) => {
|
||||
modifiedAst: Program
|
||||
|
Reference in New Issue
Block a user