Files
modeling-app/src/lang/modifyAst.test.ts
Kurt Hutten cf52e151fb Symbols overlay (#2033)
* start of overlay work

* add new icons

* add constraint symbols

* add three dots

* add primary colours

* refactor how we get constraint info for overlays

* refactor how we get constraint info for overlays

* get symbols working for tangential arc too

* extra data on constraint info

* add initial delete

* fix types and circular dep issue after rebase

* fix quirk with horz vert line overlays

* fix setup and tear down of overlays

* remove overlays that are too small

* throttle overlay updates and prove tests selecting html instead of hardcoded px coords

* initial show overaly on segment hover

* remove overlays when tool is equipped

* dounce overlay updates

* tsc

* make higlighting robust to small changes in source ranges

* replace with variable for unconstrained values, and improve styles for popover

* background tweak

* make overlays unconstrain inputs

* fix small regression

* write query for finding related tag references

* make delete segment safe

* typo

* un used imports

* test deleteSegmentFromPipeExpression

* add getConstraintInfo test

* test removeSingleConstraintInfo

* more tests

* tsc

* add tests for overlay buttons

* rename tests

* fmt

* better naming structure

* more reliablity

* more test tweaks

* fix selection test

* add delete segments with overlays tests

* dependant tag tests for segment delet

* typo

* test clean up

* fix some perf issus

* clean up

* clean up

* make things a little more dry

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* trigger ci

* Make constraint hover popovers readable on light mode

* Touch up the new variable dialog

* Little touch-up to three-dot menu style

* fix highlight issue

* fmt

* use optional chain

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit be3d61e4a3.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* disable var panel in sketch mode

* fix overlay tests after mergi in main

* test tweak

* try fix ubuntu

* fmt

* more test tweaks

* tweak

* tweaks

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-05-24 20:54:42 +10:00

597 lines
20 KiB
TypeScript

import { parse, recast, initPromise, Identifier } from './wasm'
import {
createLiteral,
createIdentifier,
createCallExpression,
createObjectExpression,
createArrayExpression,
createPipeSubstitution,
createVariableDeclaration,
createPipeExpression,
findUniqueName,
addSketchTo,
giveSketchFnCallTag,
moveValueIntoNewVariable,
sketchOnExtrudedFace,
deleteSegmentFromPipeExpression,
removeSingleConstraintInfo,
} from './modifyAst'
import { enginelessExecutor } from '../lib/testHelpers'
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
beforeAll(async () => {
await initPromise
})
describe('Testing createLiteral', () => {
it('should create a literal', () => {
const result = createLiteral(5)
expect(result.type).toBe('Literal')
expect(result.value).toBe(5)
})
})
describe('Testing createIdentifier', () => {
it('should create an identifier', () => {
const result = createIdentifier('myVar')
expect(result.type).toBe('Identifier')
expect(result.name).toBe('myVar')
})
})
describe('Testing createCallExpression', () => {
it('should create a call expression', () => {
const result = createCallExpression('myFunc', [createLiteral(5)])
expect(result.type).toBe('CallExpression')
expect(result.callee.type).toBe('Identifier')
expect(result.callee.name).toBe('myFunc')
expect(result.arguments[0].type).toBe('Literal')
expect((result.arguments[0] as any).value).toBe(5)
})
})
describe('Testing createObjectExpression', () => {
it('should create an object expression', () => {
const result = createObjectExpression({
myProp: createLiteral(5),
})
expect(result.type).toBe('ObjectExpression')
expect(result.properties[0].type).toBe('ObjectProperty')
expect(result.properties[0].key.name).toBe('myProp')
expect(result.properties[0].value.type).toBe('Literal')
expect((result.properties[0].value as any).value).toBe(5)
})
})
describe('Testing createArrayExpression', () => {
it('should create an array expression', () => {
const result = createArrayExpression([createLiteral(5)])
expect(result.type).toBe('ArrayExpression')
expect(result.elements[0].type).toBe('Literal')
expect((result.elements[0] as any).value).toBe(5)
})
})
describe('Testing createPipeSubstitution', () => {
it('should create a pipe substitution', () => {
const result = createPipeSubstitution()
expect(result.type).toBe('PipeSubstitution')
})
})
describe('Testing createVariableDeclaration', () => {
it('should create a variable declaration', () => {
const result = createVariableDeclaration('myVar', createLiteral(5))
expect(result.type).toBe('VariableDeclaration')
expect(result.declarations[0].type).toBe('VariableDeclarator')
expect(result.declarations[0].id.type).toBe('Identifier')
expect(result.declarations[0].id.name).toBe('myVar')
expect(result.declarations[0].init.type).toBe('Literal')
expect((result.declarations[0].init as any).value).toBe(5)
})
})
describe('Testing createPipeExpression', () => {
it('should create a pipe expression', () => {
const result = createPipeExpression([createLiteral(5)])
expect(result.type).toBe('PipeExpression')
expect(result.body[0].type).toBe('Literal')
expect((result.body[0] as any).value).toBe(5)
})
})
describe('Testing findUniqueName', () => {
it('should find a unique name', () => {
const result = findUniqueName(
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',
2
)
expect(result).toBe('yo10')
})
})
describe('Testing addSketchTo', () => {
it('should add a sketch to a program', () => {
const result = addSketchTo(
{
body: [],
start: 0,
end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [] },
},
'yz'
)
const str = recast(result.modifiedAst)
expect(str).toBe(`const part001 = startSketchOn('YZ')
|> startProfileAt('default', %)
|> line('default', %)
`)
})
})
function giveSketchFnCallTagTestHelper(
code: string,
searchStr: string
): { tag: string; newCode: string; isTagExisting: boolean } {
// giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing
// this wrapper changes the input and output to code
// making it more of an integration test, but easier to read the test intention is the goal
const ast = parse(code)
const start = code.indexOf(searchStr)
const range: [number, number] = [start, start + searchStr.length]
const { modifiedAst, tag, isTagExisting } = giveSketchFnCallTag(ast, range)
const newCode = recast(modifiedAst)
return { tag, newCode, isTagExisting }
}
describe('Testing giveSketchFnCallTag', () => {
const code = `const part001 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([-2.57, -0.13], %)
|> line([0, 0.83], %)
|> line([0.82, 0.34], %)`
it('Should add tag to a sketch function call', () => {
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
code,
'line([0, 0.83], %)'
)
expect(newCode).toContain("line([0, 0.83], %, 'seg01')")
expect(tag).toBe('seg01')
expect(isTagExisting).toBe(false)
})
it('Should create a unique tag if seg01 already exists', () => {
let _code = code.replace(
'line([-2.57, -0.13], %)',
"line([-2.57, -0.13], %, 'seg01')"
)
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
_code,
'line([0, 0.83], %)'
)
expect(newCode).toContain("line([0, 0.83], %, 'seg02')")
expect(tag).toBe('seg02')
expect(isTagExisting).toBe(false)
})
it('Should return existing tag if it already exists', () => {
const lineButWithTag = "line([-2.57, -0.13], %, 'butts')"
let _code = code.replace('line([-2.57, -0.13], %)', lineButWithTag)
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
_code,
lineButWithTag
)
expect(newCode).toContain(lineButWithTag) // no change
expect(tag).toBe('butts')
expect(isTagExisting).toBe(true)
})
})
describe('Testing moveValueIntoNewVariable', () => {
const fn = (fnName: string) => `fn ${fnName} = (x) => {
return x
}
`
const code = `${fn('def')}${fn('jkl')}${fn('hmm')}
fn ghi = (x) => {
return 2
}
const abc = 3
const identifierGuy = 5
const yo = 5 + 6
const part001 = startSketchOn('XY')
|> startProfileAt([-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 yo2 = hmm([identifierGuy + 5])`
it('should move a binary expression into a new variable', async () => {
const ast = parse(code)
const programMemory = await enginelessExecutor(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', async () => {
const ast = parse(code)
const programMemory = await enginelessExecutor(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 callExpression into a new variable', async () => {
const ast = parse(code)
const programMemory = await enginelessExecutor(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 binary expression with call expression into a new variable', async () => {
const ast = parse(code)
const programMemory = await enginelessExecutor(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 identifier into a new variable', async () => {
const ast = parse(code)
const programMemory = await enginelessExecutor(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])`)
})
})
describe('testing sketchOnExtrudedFace', () => {
test('it should be able to extrude on regular segments', async () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const snippet = `line([9.7, 9.19], %)`
const range: [number, number] = [
code.indexOf(snippet),
code.indexOf(snippet) + snippet.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const { modifiedAst } = sketchOnExtrudedFace(ast, pathToNode, programMemory)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %, 'seg01')
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01')`)
})
test('it should be able to extrude on close segments', async () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const snippet = `close(%)`
const range: [number, number] = [
code.indexOf(snippet),
code.indexOf(snippet) + snippet.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const { modifiedAst } = sketchOnExtrudedFace(ast, pathToNode, programMemory)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%, 'seg01')
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01')`)
})
test('it should be able to extrude on start-end caps', async () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const snippet = `startProfileAt([3.58, 2.06], %)`
const range: [number, number] = [
code.indexOf(snippet),
code.indexOf(snippet) + snippet.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const { modifiedAst } = sketchOnExtrudedFace(
ast,
pathToNode,
programMemory,
'end'
)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'END')`)
})
})
describe('Testing deleteSegmentFromPipeExpression', () => {
it('Should delete a segment withOUT any dependent segments', async () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([54.78, -95.91], %)
|> line([306.21, 198.82], %)
|> line([306.21, 198.85], %, 'a')
|> line([306.21, 198.87], %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = "line([306.21, 198.85], %, 'a')"
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const modifiedAst = deleteSegmentFromPipeExpression(
[],
ast,
programMemory,
code,
pathToNode
)
const newCode = recast(modifiedAst)
expect(newCode).toBe(`const part001 = startSketchOn('-XZ')
|> startProfileAt([54.78, -95.91], %)
|> line([306.21, 198.82], %)
|> line([306.21, 198.87], %)
`)
})
describe('Should delete a segment WITH any dependent segments, unconstraining the dependent parts', () => {
const makeCode = (
line: string,
replace1 = '',
replace2 = ''
) => `const part001 = startSketchOn('-XZ')
|> startProfileAt([54.78, -95.91], %)
|> line([306.21, 198.82], %, 'b')
${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
!replace1 ? "segLen('a', %)" : replace1
}], %)
|> line([306.21, 198.87], %)
|> angledLine([65, ${!replace2 ? "segAng('a', %)" : replace2}], %)
|> line([-963.39, -154.67], %)
`
test.each([
['line', "line([306.21, 198.85], %, 'a')", ['365.11', '33']],
['lineTo', "lineTo([306.21, 198.85], %, 'a')", ['110.48', '119.73']],
['yLine', "yLine(198.85, %, 'a')", ['198.85', '90']],
['xLine', "xLine(198.85, %, 'a')", ['198.85', '0']],
['yLineTo', "yLineTo(198.85, %, 'a')", ['95.94', '90']],
['xLineTo', "xLineTo(198.85, %, 'a')", ['162.14', '180']],
[
'angledLine',
"angledLine({ angle: 45.5, length: 198.85 }, %, 'a')",
['198.85', '45.5'],
],
[
'angledLineOfXLength',
"angledLineOfXLength({ angle: 45.5, length: 198.85 }, %, 'a')",
['283.7', '45.5'],
],
[
'angledLineOfYLength',
"angledLineOfYLength({ angle: 45.5, length: 198.85 }, %, 'a')",
['278.79', '45.5'],
],
[
'angledLineToX',
"angledLineToX({ angle: 45.5, to: 198.85 }, %, 'a')",
['231.33', '134.5'],
],
[
'angledLineToY',
"angledLineToY({ angle: 45.5, to: 198.85 }, %, 'a')",
['134.51', '45.5'],
],
[
'angledLineThatIntersects',
`angledLineThatIntersects({ angle: 45.5, intersectTag: 'b', offset: 198.85 }, %, 'a')`,
['918.4', '45.5'],
],
])(`%s`, async (_, line, [replace1, replace2]) => {
const code = makeCode(line)
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = line
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
const modifiedAst = deleteSegmentFromPipeExpression(
dependentSegments,
ast,
programMemory,
code,
pathToNode
)
const newCode = recast(modifiedAst)
expect(newCode).toBe(makeCode(line, replace1, replace2))
})
})
})
describe('Testing removeSingleConstraintInfo', () => {
describe('with mostly object notation', () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> line([3 + 0, 4 + 0], %)
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|> lineTo([6.14 + 0, 3.14 + 0], %)
|> xLineTo(8 + 0, %)
|> yLineTo(5 + 0, %)
|> yLine(3.14 + 0, %, 'a')
|> xLine(3.14 + 0, %)
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 + 0 }, %)
|> angledLineOfYLength({ angle: 30 + 0, length: 3 + 0 }, %)
|> angledLineToX({ angle: 12.14 + 0, to: 12 + 0 }, %)
|> angledLineToY({ angle: 30 + 0, to: 10.14 + 0 }, %)
|> angledLineThatIntersects({
angle: 3.14 + 0,
intersectTag: 'a',
offset: 0 + 0
}, %)
|> tangentialArcTo([3.14 + 0, 13.14 + 0], %)`
test.each([
[' line([3 + 0, 4], %)', 'arrayIndex', 1],
[
'angledLine({ angle: 3, length: 3.14 + 0 }, %)',
'objectProperty',
'angle',
],
['lineTo([6.14, 3.14 + 0], %)', 'arrayIndex', 0],
['xLineTo(8, %)', '', ''],
['yLineTo(5, %)', '', ''],
["yLine(3.14, %, 'a')", '', ''],
['xLine(3.14, %)', '', ''],
[
'angledLineOfXLength({ angle: 3, length: 3.14 + 0 }, %)',
'objectProperty',
'angle',
],
[
'angledLineOfYLength({ angle: 30 + 0, length: 3 }, %)',
'objectProperty',
'length',
],
[
'angledLineToX({ angle: 12.14 + 0, to: 12 }, %)',
'objectProperty',
'to',
],
[
'angledLineToY({ angle: 30, to: 10.14 + 0 }, %)',
'objectProperty',
'angle',
],
[
`angledLineThatIntersects({
angle: 3.14 + 0,
offset: 0,
intersectTag: 'a'
}, %)`,
'objectProperty',
'offset',
],
['tangentialArcTo([3.14 + 0, 13.14], %)', 'arrayIndex', 1],
])('stdlib fn: %s', async (expectedFinish, key, value) => {
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const mod = removeSingleConstraintInfo(
{
pathToCallExp: pathToNode,
[key]: value,
},
ast,
programMemory
)
if (!mod) throw new Error('yo is undefined')
const recastCode = recast(mod.modifiedAst)
expect(recastCode).toContain(expectedFinish)
})
})
describe('with array notation', () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> angledLine([3.14 + 0, 3.14 + 0], %)
|> angledLineOfXLength([3 + 0, 3.14 + 0], %)
|> angledLineOfYLength([30 + 0, 3 + 0], %)
|> angledLineToX([12.14 + 0, 12 + 0], %)
|> angledLineToY([30 + 0, 10.14 + 0], %)`
test.each([
['angledLine([3, 3.14 + 0], %)', 'arrayIndex', 0],
['angledLineOfXLength([3, 3.14 + 0], %)', 'arrayIndex', 0],
['angledLineOfYLength([30 + 0, 3], %)', 'arrayIndex', 1],
['angledLineToX([12.14 + 0, 12], %)', 'arrayIndex', 1],
['angledLineToY([30, 10.14 + 0], %)', 'arrayIndex', 0],
])('stdlib fn: %s', async (expectedFinish, key, value) => {
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const mod = removeSingleConstraintInfo(
{
pathToCallExp: pathToNode,
[key]: value,
},
ast,
programMemory
)
if (!mod) throw new Error('yo is undefined')
const recastCode = recast(mod.modifiedAst)
expect(recastCode).toContain(expectedFinish)
})
})
})