From cc9eb6545685d3dd7f9385e018b9847b18f894ad Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Wed, 2 Jul 2025 17:00:17 -0400 Subject: [PATCH 01/15] Fix test --- package-lock.json | 19 ++++++++++++++++++- src/lang/queryAst.ts | 10 ++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 798907d5d..f8ae0ebee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26640,10 +26640,27 @@ "vscode-uri": "^3.1.0" }, "devDependencies": { - "@types/node": "^22.14.1", + "@types/node": "^24.0.7", "ts-node": "^10.9.2" } }, + "packages/codemirror-lsp-client/node_modules/@types/node": { + "version": "24.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "packages/codemirror-lsp-client/node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "rust/kcl-language-server": { "version": "0.0.0", "license": "MIT", diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index 904d739c8..e07ead587 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -1079,16 +1079,18 @@ export function getVariableExprsFromSelection( // Pointing to same variable case paths.push(nodeToEdit) exprs.push(createPipeSubstitution()) + continue } } // Pointing to different variable case paths.push(sketchVariable.deepPath) exprs.push(createLocalName(name)) - } else { - // No variable case - paths.push(sketchVariable.deepPath) - exprs.push(createPipeSubstitution()) + continue } + + // No variable case + paths.push(sketchVariable.deepPath) + exprs.push(createPipeSubstitution()) } if (exprs.length === 0) { From cb976ec31ba2bb109b75226d34de0030d2e054ba Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Wed, 2 Jul 2025 17:42:40 -0400 Subject: [PATCH 02/15] Fix circ dep --- src/lang/modifyAst.ts | 38 +++++++++++++++++++++++++++++ src/lang/modifyAst/addSweep.ts | 8 ++++--- src/lang/queryAst.ts | 44 +--------------------------------- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index df999321c..04f2e8381 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -1209,3 +1209,41 @@ export function insertVariableAndOffsetPathToNode( } } } + +// Create an array expression for variables, +// or keep it null if all are PipeSubstitutions +export function createVariableExpressionsArray(sketches: Expr[]) { + let sketchesExpr: Expr | null = null + if (sketches.every((s) => s.type === 'PipeSubstitution')) { + // Keeping null so we don't even put it the % sign + } else if (sketches.length === 1) { + sketchesExpr = sketches[0] + } else { + sketchesExpr = createArrayExpression(sketches) + } + return sketchesExpr +} + +// Create a path to node to the last variable declaroator of an ast +// Optionally, can point to the first kwarg of the CallExpressionKw +export function createPathToNodeForLastVariable( + ast: Node, + toFirstKwarg = true +): PathToNode { + const argIndex = 0 // first kwarg for all sweeps here + const pathToCall: PathToNode = [ + ['body', ''], + [ast.body.length - 1, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', 'VariableDeclarator'], + ] + if (toFirstKwarg) { + pathToCall.push( + ['arguments', 'CallExpressionKw'], + [argIndex, ARG_INDEX_FIELD], + ['arg', LABELED_ARG_FIELD] + ) + } + + return pathToCall +} diff --git a/src/lang/modifyAst/addSweep.ts b/src/lang/modifyAst/addSweep.ts index ba53bad50..3f39c2c50 100644 --- a/src/lang/modifyAst/addSweep.ts +++ b/src/lang/modifyAst/addSweep.ts @@ -8,7 +8,11 @@ import { createVariableDeclaration, findUniqueName, } from '@src/lang/create' -import { insertVariableAndOffsetPathToNode } from '@src/lang/modifyAst' +import { + createPathToNodeForLastVariable, + createVariableExpressionsArray, + insertVariableAndOffsetPathToNode, +} from '@src/lang/modifyAst' import { getEdgeTagCall, mutateAstWithTagForSketchSegment, @@ -16,8 +20,6 @@ import { import { getNodeFromPath, getVariableExprsFromSelection, - createVariableExpressionsArray, - createPathToNodeForLastVariable, valueOrVariable, } from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index e07ead587..11486384a 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -2,11 +2,7 @@ import type { FunctionExpression } from '@rust/kcl-lib/bindings/FunctionExpressi import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement' import type { Node } from '@rust/kcl-lib/bindings/Node' import type { TypeDeclaration } from '@rust/kcl-lib/bindings/TypeDeclaration' -import { - createLocalName, - createPipeSubstitution, - createArrayExpression, -} from '@src/lang/create' +import { createLocalName, createPipeSubstitution } from '@src/lang/create' import type { ToolTip } from '@src/lang/langHelpers' import { splitPathAtLastIndex } from '@src/lang/modifyAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' @@ -1100,44 +1096,6 @@ export function getVariableExprsFromSelection( return { exprs, paths } } -// Create an array expression for variables, -// or keep it null if all are PipeSubstitutions -export function createVariableExpressionsArray(sketches: Expr[]) { - let sketchesExpr: Expr | null = null - if (sketches.every((s) => s.type === 'PipeSubstitution')) { - // Keeping null so we don't even put it the % sign - } else if (sketches.length === 1) { - sketchesExpr = sketches[0] - } else { - sketchesExpr = createArrayExpression(sketches) - } - return sketchesExpr -} - -// Create a path to node to the last variable declaroator of an ast -// Optionally, can point to the first kwarg of the CallExpressionKw -export function createPathToNodeForLastVariable( - ast: Node, - toFirstKwarg = true -): PathToNode { - const argIndex = 0 // first kwarg for all sweeps here - const pathToCall: PathToNode = [ - ['body', ''], - [ast.body.length - 1, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', 'VariableDeclarator'], - ] - if (toFirstKwarg) { - pathToCall.push( - ['arguments', 'CallExpressionKw'], - [argIndex, ARG_INDEX_FIELD], - ['arg', LABELED_ARG_FIELD] - ) - } - - return pathToCall -} - // Go from the sketches argument in a KCL sweep call declaration // to a list of graph selections, useful for edit flows. // Somewhat of an inverse of getSketchExprsFromSelection. From d4d3e179b19ffb1207ab69c43d2d19a2961bff6a Mon Sep 17 00:00:00 2001 From: alteous Date: Wed, 2 Jul 2025 23:04:03 +0100 Subject: [PATCH 03/15] Update test data (#7674) --- ...elix_defaults_negative_extrude_output.step | 131 +++++++++--------- 1 file changed, 68 insertions(+), 63 deletions(-) diff --git a/rust/kcl-lib/e2e/executor/outputs/helix_defaults_negative_extrude_output.step b/rust/kcl-lib/e2e/executor/outputs/helix_defaults_negative_extrude_output.step index 7b62dba36..c54ab21d7 100644 --- a/rust/kcl-lib/e2e/executor/outputs/helix_defaults_negative_extrude_output.step +++ b/rust/kcl-lib/e2e/executor/outputs/helix_defaults_negative_extrude_output.step @@ -10,71 +10,76 @@ DATA; NAMED_UNIT(*) SI_UNIT($, .METRE.) ); -#2 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $); -#3 = ( +#2 = ( + NAMED_UNIT(*) + PLANE_ANGLE_UNIT() + SI_UNIT($, .RADIAN.) +); +#3 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $); +#4 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) - GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2)) - GLOBAL_UNIT_ASSIGNED_CONTEXT((#1)) + GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3)) + GLOBAL_UNIT_ASSIGNED_CONTEXT((#1, #2)) REPRESENTATION_CONTEXT('', '3D') ); -#4 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); -#5 = VERTEX_POINT('NONE', #4); -#6 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005)); -#7 = VERTEX_POINT('NONE', #6); -#8 = DIRECTION('NONE', (1, 0, -0)); -#9 = DIRECTION('NONE', (0, 1, 0)); -#10 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005)); -#11 = AXIS2_PLACEMENT_3D('NONE', #10, #9, #8); -#12 = CIRCLE('NONE', #11, 0.01); -#13 = DIRECTION('NONE', (0, 1, 0)); -#14 = VECTOR('NONE', #13, 1); -#15 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); -#16 = LINE('NONE', #15, #14); -#17 = DIRECTION('NONE', (1, 0, -0)); -#18 = DIRECTION('NONE', (0, 1, 0)); -#19 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005)); -#20 = AXIS2_PLACEMENT_3D('NONE', #19, #18, #17); -#21 = CIRCLE('NONE', #20, 0.01); -#22 = EDGE_CURVE('NONE', #5, #5, #12, .T.); -#23 = EDGE_CURVE('NONE', #5, #7, #16, .T.); -#24 = EDGE_CURVE('NONE', #7, #7, #21, .T.); -#25 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005)); -#26 = DIRECTION('NONE', (0, 1, 0)); -#27 = DIRECTION('NONE', (1, 0, -0)); -#28 = AXIS2_PLACEMENT_3D('NONE', #25, #26, #27); -#29 = CYLINDRICAL_SURFACE('NONE', #28, 0.01); -#30 = CARTESIAN_POINT('NONE', (0, -0.01, -0)); -#31 = DIRECTION('NONE', (0, 1, 0)); -#32 = AXIS2_PLACEMENT_3D('NONE', #30, #31, $); -#33 = PLANE('NONE', #32); -#34 = CARTESIAN_POINT('NONE', (0, 0, -0)); -#35 = DIRECTION('NONE', (0, 1, 0)); -#36 = AXIS2_PLACEMENT_3D('NONE', #34, #35, $); -#37 = PLANE('NONE', #36); -#38 = ORIENTED_EDGE('NONE', *, *, #22, .T.); -#39 = ORIENTED_EDGE('NONE', *, *, #24, .F.); -#40 = EDGE_LOOP('NONE', (#38)); -#41 = FACE_BOUND('NONE', #40, .T.); -#42 = EDGE_LOOP('NONE', (#39)); -#43 = FACE_BOUND('NONE', #42, .T.); -#44 = ADVANCED_FACE('NONE', (#41, #43), #29, .T.); -#45 = ORIENTED_EDGE('NONE', *, *, #22, .F.); -#46 = EDGE_LOOP('NONE', (#45)); -#47 = FACE_BOUND('NONE', #46, .T.); -#48 = ADVANCED_FACE('NONE', (#47), #33, .F.); -#49 = ORIENTED_EDGE('NONE', *, *, #24, .T.); -#50 = EDGE_LOOP('NONE', (#49)); -#51 = FACE_BOUND('NONE', #50, .T.); -#52 = ADVANCED_FACE('NONE', (#51), #37, .T.); -#53 = CLOSED_SHELL('NONE', (#44, #48, #52)); -#54 = MANIFOLD_SOLID_BREP('NONE', #53); -#55 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies'); -#56 = PRODUCT_DEFINITION_CONTEXT('part definition', #55, 'design'); -#57 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ()); -#58 = PRODUCT_DEFINITION_FORMATION('', $, #57); -#59 = PRODUCT_DEFINITION('design', $, #58, #56); -#60 = PRODUCT_DEFINITION_SHAPE('NONE', $, #59); -#61 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#54), #3); -#62 = SHAPE_DEFINITION_REPRESENTATION(#60, #61); +#5 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); +#6 = VERTEX_POINT('NONE', #5); +#7 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005)); +#8 = VERTEX_POINT('NONE', #7); +#9 = DIRECTION('NONE', (1, 0, -0)); +#10 = DIRECTION('NONE', (0, 1, 0)); +#11 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005)); +#12 = AXIS2_PLACEMENT_3D('NONE', #11, #10, #9); +#13 = CIRCLE('NONE', #12, 0.01); +#14 = DIRECTION('NONE', (0, 1, 0)); +#15 = VECTOR('NONE', #14, 1); +#16 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); +#17 = LINE('NONE', #16, #15); +#18 = DIRECTION('NONE', (1, 0, -0)); +#19 = DIRECTION('NONE', (0, 1, 0)); +#20 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005)); +#21 = AXIS2_PLACEMENT_3D('NONE', #20, #19, #18); +#22 = CIRCLE('NONE', #21, 0.01); +#23 = EDGE_CURVE('NONE', #6, #6, #13, .T.); +#24 = EDGE_CURVE('NONE', #6, #8, #17, .T.); +#25 = EDGE_CURVE('NONE', #8, #8, #22, .T.); +#26 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005)); +#27 = DIRECTION('NONE', (0, 1, 0)); +#28 = DIRECTION('NONE', (1, 0, -0)); +#29 = AXIS2_PLACEMENT_3D('NONE', #26, #27, #28); +#30 = CYLINDRICAL_SURFACE('NONE', #29, 0.01); +#31 = CARTESIAN_POINT('NONE', (0, -0.01, -0)); +#32 = DIRECTION('NONE', (0, 1, 0)); +#33 = AXIS2_PLACEMENT_3D('NONE', #31, #32, $); +#34 = PLANE('NONE', #33); +#35 = CARTESIAN_POINT('NONE', (0, 0, -0)); +#36 = DIRECTION('NONE', (0, 1, 0)); +#37 = AXIS2_PLACEMENT_3D('NONE', #35, #36, $); +#38 = PLANE('NONE', #37); +#39 = ORIENTED_EDGE('NONE', *, *, #23, .T.); +#40 = ORIENTED_EDGE('NONE', *, *, #25, .F.); +#41 = EDGE_LOOP('NONE', (#39)); +#42 = FACE_BOUND('NONE', #41, .T.); +#43 = EDGE_LOOP('NONE', (#40)); +#44 = FACE_BOUND('NONE', #43, .T.); +#45 = ADVANCED_FACE('NONE', (#42, #44), #30, .T.); +#46 = ORIENTED_EDGE('NONE', *, *, #23, .F.); +#47 = EDGE_LOOP('NONE', (#46)); +#48 = FACE_BOUND('NONE', #47, .T.); +#49 = ADVANCED_FACE('NONE', (#48), #34, .F.); +#50 = ORIENTED_EDGE('NONE', *, *, #25, .T.); +#51 = EDGE_LOOP('NONE', (#50)); +#52 = FACE_BOUND('NONE', #51, .T.); +#53 = ADVANCED_FACE('NONE', (#52), #38, .T.); +#54 = CLOSED_SHELL('NONE', (#45, #49, #53)); +#55 = MANIFOLD_SOLID_BREP('NONE', #54); +#56 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies'); +#57 = PRODUCT_DEFINITION_CONTEXT('part definition', #56, 'design'); +#58 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ()); +#59 = PRODUCT_DEFINITION_FORMATION('', $, #58); +#60 = PRODUCT_DEFINITION('design', $, #59, #57); +#61 = PRODUCT_DEFINITION_SHAPE('NONE', $, #60); +#62 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#55), #4); +#63 = SHAPE_DEFINITION_REPRESENTATION(#61, #62); ENDSEC; END-ISO-10303-21; From e5d082f441d749cb54943fad00beb59af7c3a4f7 Mon Sep 17 00:00:00 2001 From: Kevin Nadro Date: Wed, 2 Jul 2025 17:51:01 -0500 Subject: [PATCH 04/15] [Chore] Removing old confusing BASE_URL environment variable. (#7678) chore: this is deprecated, we should be using the VITE_KC_*_BASE_URL workflows --- interface.d.ts | 1 - src/main.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/interface.d.ts b/interface.d.ts index 1a2904e2b..272da2054 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -72,7 +72,6 @@ export interface IElectronAPI { } process: { env: { - BASE_URL: string IS_PLAYWRIGHT: string VITE_KC_DEV_TOKEN: string VITE_KC_API_WS_MODELING_URL: string diff --git a/src/main.ts b/src/main.ts index c130963b9..cf3533f0f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -70,7 +70,6 @@ dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] }) // default vite values based on mode process.env.NODE_ENV ??= viteEnv.MODE -process.env.BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL process.env.VITE_KC_API_BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL process.env.VITE_KC_SITE_BASE_URL ??= viteEnv.VITE_KC_SITE_BASE_URL From c467568ee4a623138b969ae1e1ba518df58672ee Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Wed, 2 Jul 2025 19:26:28 -0400 Subject: [PATCH 05/15] Add unit tests for getVariableExprsFromSelection --- src/lang/queryAst.test.ts | 184 +++++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/src/lang/queryAst.test.ts b/src/lang/queryAst.test.ts index 94d570af7..74d6cf9ca 100644 --- a/src/lang/queryAst.test.ts +++ b/src/lang/queryAst.test.ts @@ -15,6 +15,7 @@ import { findAllPreviousVariables, findUsesOfTagInPipe, getNodeFromPath, + getVariableExprsFromSelection, hasSketchPipeBeenExtruded, isCursorInFunctionDefinition, isNodeSafeToReplace, @@ -27,7 +28,7 @@ import { topLevelRange } from '@src/lang/util' import type { Identifier, PathToNode } from '@src/lang/wasm' import { assertParse, recast } from '@src/lang/wasm' import { initPromise } from '@src/lang/wasmUtils' -import { type Selection } from '@src/lib/selections' +import type { Selections, Selection } from '@src/lib/selections' import { enginelessExecutor } from '@src/lib/testHelpers' import { err } from '@src/lib/trap' @@ -778,3 +779,184 @@ describe('Testing specific sketch getNodeFromPath workflow', () => { expect(result).toEqual(false) }) }) + +describe('Testing getVariableExprsFromSelection', () => { + it('should find the variable expr in a simple profile selection', async () => { + const circleProfileInVar = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +` + const ast = assertParse(circleProfileInVar) + const { artifactGraph } = await enginelessExecutor(ast) + const artifact = artifactGraph.values().find((a) => a.type === 'path') + if (!artifact) { + throw new Error('Artifact not found in the graph') + } + const selections: Selections = { + graphSelections: [ + { + codeRef: artifact.codeRef, + artifact, + }, + ], + otherSelections: [], + } + const variableExprs = getVariableExprsFromSelection(selections, ast) + if (err(variableExprs)) throw variableExprs + + expect(variableExprs.exprs).toHaveLength(1) + if (variableExprs.exprs[0].type !== 'Name') { + throw new Error(`Expected Name, got ${variableExprs.exprs[0].type}`) + } + + expect(variableExprs.exprs[0].name.name).toEqual('profile001') + + expect(variableExprs.paths).toHaveLength(1) + expect(variableExprs.paths[0]).toEqual([ + ['body', ''], + [1, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', ''], + ]) + }) + + it('should return the pipe substition symbol in a variable-less simple profile selection', async () => { + const circleProfileInVar = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) +` + const ast = assertParse(circleProfileInVar) + const { artifactGraph } = await enginelessExecutor(ast) + const artifact = artifactGraph.values().find((a) => a.type === 'path') + if (!artifact) { + throw new Error('Artifact not found in the graph') + } + const selections: Selections = { + graphSelections: [ + { + codeRef: artifact.codeRef, + artifact, + }, + ], + otherSelections: [], + } + const variableExprs = getVariableExprsFromSelection(selections, ast) + if (err(variableExprs)) throw variableExprs + + expect(variableExprs.exprs).toHaveLength(1) + expect(variableExprs.exprs[0].type).toEqual('PipeSubstitution') + + expect(variableExprs.paths).toHaveLength(1) + expect(variableExprs.paths[0]).toEqual([ + ['body', ''], + [0, 'index'], + ['expression', 'ExpressionStatement'], + ['body', 'PipeExpression'], + [1, 'index'], + ]) + }) + + it('should find the variable exprs in a multi profile selection ', async () => { + const circleProfileInVar = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +profile002 = circle(sketch001, center = [2, 2], radius = 1) +` + const ast = assertParse(circleProfileInVar) + const { artifactGraph } = await enginelessExecutor(ast) + const artifacts = [...artifactGraph.values()].filter( + (a) => a.type === 'path' + ) + if (!artifacts || artifacts.length !== 2) { + throw new Error('Artifact not found in the graph') + } + const selections: Selections = { + graphSelections: artifacts.map((artifact) => { + return { + codeRef: artifact.codeRef, + artifact, + } + }), + otherSelections: [], + } + const variableExprs = getVariableExprsFromSelection(selections, ast) + if (err(variableExprs)) throw variableExprs + + expect(variableExprs.exprs).toHaveLength(2) + if (variableExprs.exprs[0].type !== 'Name') { + throw new Error(`Expected Name, got ${variableExprs.exprs[0].type}`) + } + + if (variableExprs.exprs[1].type !== 'Name') { + throw new Error(`Expected Name, got ${variableExprs.exprs[1].type}`) + } + + expect(variableExprs.exprs[0].name.name).toEqual('profile001') + expect(variableExprs.exprs[1].name.name).toEqual('profile002') + + expect(variableExprs.paths).toHaveLength(2) + expect(variableExprs.paths[0]).toEqual([ + ['body', ''], + [1, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', ''], + ]) + expect(variableExprs.paths[1]).toEqual([ + ['body', ''], + [2, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', ''], + ]) + }) + + it('should return the pipe substition symbol and a variable name in a complex multi profile selection', async () => { + const circleProfileInVar = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) +profile002 = circle(sketch001, center = [2, 2], radius = 1) +` + const ast = assertParse(circleProfileInVar) + const { artifactGraph } = await enginelessExecutor(ast) + const artifacts = [...artifactGraph.values()].filter( + (a) => a.type === 'path' + ) + if (!artifacts || artifacts.length !== 2) { + throw new Error('Artifact not found in the graph') + } + const selections: Selections = { + graphSelections: artifacts.map((artifact) => { + return { + codeRef: artifact.codeRef, + artifact, + } + }), + otherSelections: [], + } + const variableExprs = getVariableExprsFromSelection(selections, ast) + if (err(variableExprs)) throw variableExprs + + expect(variableExprs.exprs).toHaveLength(2) + if (variableExprs.exprs[0].type !== 'PipeSubstitution') { + throw new Error( + `Expected PipeSubstitution, got ${variableExprs.exprs[0].type}` + ) + } + + if (variableExprs.exprs[1].type !== 'Name') { + throw new Error(`Expected Name, got ${variableExprs.exprs[1].type}`) + } + + expect(variableExprs.exprs[1].name.name).toEqual('profile002') + + expect(variableExprs.paths).toHaveLength(2) + expect(variableExprs.paths[0]).toEqual([ + ['body', ''], + [0, 'index'], + ['expression', 'ExpressionStatement'], + ['body', 'PipeExpression'], + [1, 'index'], + ]) + expect(variableExprs.paths[1]).toEqual([ + ['body', ''], + [1, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', ''], + ]) + }) +}) From dd9b0ec5f0dc39aa75dad3615075a9735125bcc2 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Wed, 2 Jul 2025 19:38:34 -0400 Subject: [PATCH 06/15] Codespell --- src/lang/queryAst.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/queryAst.test.ts b/src/lang/queryAst.test.ts index 74d6cf9ca..2039b7ea1 100644 --- a/src/lang/queryAst.test.ts +++ b/src/lang/queryAst.test.ts @@ -819,7 +819,7 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) ]) }) - it('should return the pipe substition symbol in a variable-less simple profile selection', async () => { + it('should return the pipe substitution symbol in a variable-less simple profile selection', async () => { const circleProfileInVar = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) ` @@ -906,7 +906,7 @@ profile002 = circle(sketch001, center = [2, 2], radius = 1) ]) }) - it('should return the pipe substition symbol and a variable name in a complex multi profile selection', async () => { + it('should return the pipe substitution symbol and a variable name in a complex multi profile selection', async () => { const circleProfileInVar = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) profile002 = circle(sketch001, center = [2, 2], radius = 1) From 5b8284e737692dda79c5e3e0dc2606b1f8cbb495 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 07:44:29 -0400 Subject: [PATCH 07/15] Add unit tests for createVariableExpressionsArray --- src/lang/modifyAst.test.ts | 77 ++++++++++++++++++++++++++++++++++++++ src/lang/modifyAst.ts | 10 ++--- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index f65d4c6ed..a7e7bd63a 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -5,6 +5,7 @@ import { createIdentifier, createLiteral, createLiteralMaybeSuffix, + createLocalName, createObjectExpression, createPipeExpression, createPipeSubstitution, @@ -14,6 +15,7 @@ import { } from '@src/lang/create' import { addSketchTo, + createVariableExpressionsArray, deleteSegmentFromPipeExpression, moveValueIntoNewVariable, sketchOnExtrudedFace, @@ -917,3 +919,78 @@ extrude001 = extrude(part001, length = 5) expect(result instanceof Error).toBe(true) }) }) + +describe('Testing createVariableExpressionsArray', () => { + it('should return null for any number of pipe substitutions', () => { + const onePipe = [createPipeSubstitution()] + const twoPipes = [createPipeSubstitution(), createPipeSubstitution()] + const threePipes = [ + createPipeSubstitution(), + createPipeSubstitution(), + createPipeSubstitution(), + ] + expect(createVariableExpressionsArray(onePipe)).toBeNull() + expect(createVariableExpressionsArray(twoPipes)).toBeNull() + expect(createVariableExpressionsArray(threePipes)).toBeNull() + }) + + it('should create a variable expressions for one variable', () => { + const oneVariableName = [createLocalName('var1')] + const expr = createVariableExpressionsArray(oneVariableName) + if (expr?.type !== 'Name') { + throw new Error(`Expected Literal type, got ${expr?.type}`) + } + + expect(expr.name.name).toBe('var1') + }) + + it('should create an array of variable expressions for two variables', () => { + const twoVariableNames = [createLocalName('var1'), createLocalName('var2')] + const exprs = createVariableExpressionsArray(twoVariableNames) + if (exprs?.type !== 'ArrayExpression') { + throw new Error('Expected ArrayExpression type') + } + + expect(exprs.elements).toHaveLength(2) + if ( + exprs.elements[0].type !== 'Name' || + exprs.elements[1].type !== 'Name' + ) { + throw new Error( + `Expected elements to be of type Name, got ${exprs.elements[0].type} and ${exprs.elements[1].type}` + ) + } + expect(exprs.elements[0].name.name).toBe('var1') + expect(exprs.elements[1].name.name).toBe('var2') + }) + + // This would catch the issue at https://github.com/KittyCAD/modeling-app/issues/7669 + // TODO: fix function to get this test to pass + // it('should create one expr if the array of variable names are the same', () => { + // const twoVariableNames = [createLocalName('var1'), createLocalName('var1')] + // const expr = createVariableExpressionsArray(twoVariableNames) + // if (expr?.type !== 'Name') { + // throw new Error(`Expected Literal type, got ${expr?.type}`) + // } + + // expect(expr.name.name).toBe('var1') + // }) + + it('should create an array of variable expressions for one variable and a pipe', () => { + const oneVarOnePipe = [createPipeSubstitution(), createLocalName('var1')] + const exprs = createVariableExpressionsArray(oneVarOnePipe) + if (exprs?.type !== 'ArrayExpression') { + throw new Error('Expected ArrayExpression type') + } + + expect(exprs.elements).toHaveLength(2) + expect(exprs.elements[0].type).toBe('PipeSubstitution') + if (exprs.elements[1].type !== 'Name') { + throw new Error( + `Expected elements[1] to be of type Name, got ${exprs.elements[1].type}` + ) + } + + expect(exprs.elements[1].name.name).toBe('var1') + }) +}) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 04f2e8381..da64d8d7c 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -1212,16 +1212,16 @@ export function insertVariableAndOffsetPathToNode( // Create an array expression for variables, // or keep it null if all are PipeSubstitutions -export function createVariableExpressionsArray(sketches: Expr[]) { - let sketchesExpr: Expr | null = null +export function createVariableExpressionsArray(sketches: Expr[]): Expr | null { + let exprs: Expr | null = null if (sketches.every((s) => s.type === 'PipeSubstitution')) { // Keeping null so we don't even put it the % sign } else if (sketches.length === 1) { - sketchesExpr = sketches[0] + exprs = sketches[0] } else { - sketchesExpr = createArrayExpression(sketches) + exprs = createArrayExpression(sketches) } - return sketchesExpr + return exprs } // Create a path to node to the last variable declaroator of an ast From 5708b8c64b77fff533bf1dff6ad9c4b3da0a67a4 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 08:50:08 -0400 Subject: [PATCH 08/15] Add unit test createPathToNodeForLastVariable --- src/lang/modifyAst.test.ts | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index a7e7bd63a..a420704ad 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -15,13 +15,14 @@ import { } from '@src/lang/create' import { addSketchTo, + createPathToNodeForLastVariable, createVariableExpressionsArray, deleteSegmentFromPipeExpression, moveValueIntoNewVariable, sketchOnExtrudedFace, splitPipedProfile, } from '@src/lang/modifyAst' -import { findUsesOfTagInPipe } from '@src/lang/queryAst' +import { findUsesOfTagInPipe, getNodeFromPath } from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import type { Artifact } from '@src/lang/std/artifactGraph' import { codeRefFromRange } from '@src/lang/std/artifactGraph' @@ -994,3 +995,45 @@ describe('Testing createVariableExpressionsArray', () => { expect(exprs.elements[1].name.name).toBe('var1') }) }) + +describe('Testing createPathToNodeForLastVariable', () => { + it('should create a path to the last variable in the array', () => { + const circleProfileInVar = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 5) +` + const ast = assertParse(circleProfileInVar) + const path = createPathToNodeForLastVariable(ast, false) + expect(path.length).toEqual(4) + + // Verify we can get the right node + const node = getNodeFromPath(ast, path) + if (err(node)) { + throw node + } + // With the expected range + const startOfExtrudeIndex = circleProfileInVar.indexOf('extrude(') + expect(node.node.start).toEqual(startOfExtrudeIndex) + expect(node.node.end).toEqual(circleProfileInVar.length - 1) + }) + + it('should create a path to the first kwarg in the last expression', () => { + const circleProfileInVar = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +extrude001 = extrude(profile001, length = 123) +` + const ast = assertParse(circleProfileInVar) + const path = createPathToNodeForLastVariable(ast, true) + expect(path.length).toEqual(7) + + // Verify we can get the right node + const node = getNodeFromPath(ast, path) + if (err(node)) { + throw node + } + // With the expected range + const startOfKwargIndex = circleProfileInVar.indexOf('123') + expect(node.node.start).toEqual(startOfKwargIndex) + expect(node.node.end).toEqual(startOfKwargIndex + 3) + }) +}) From df6256266cb5b58c63605d4561ded30c82f427a2 Mon Sep 17 00:00:00 2001 From: Kevin Nadro Date: Thu, 3 Jul 2025 08:54:03 -0500 Subject: [PATCH 09/15] [Chore] All api urls are now using the helper function (#7672) * fix: logging information about the login * chore: improving the withBaseURL workflow * chore: moving VITE_KC_API_BASE_URL to the helper function * fix: env to helper function api base url * chore: fixing another api base url * chore: shortlinks with base api helper function * chore: prompt edit with base helper function * fix: auto fmt * fix: withAPIBaseURL for all urls * fix: AI caught my typo, RIP * fix: expected * fix: renaming this so it is less specific to environment --------- Co-authored-by: Jace Browning --- scripts/known/urls.txt | 2 ++ src/components/LspProvider.tsx | 7 +++-- src/lib/coredump.ts | 4 +-- src/lib/desktop.ts | 12 ++++---- src/lib/links.ts | 5 ++-- src/lib/promptToEdit.tsx | 9 +++--- src/lib/singletons.ts | 4 +-- src/lib/textToCad.ts | 6 ++-- src/lib/textToCadTelemetry.ts | 5 ++-- src/lib/withBaseURL.test.ts | 34 +++++++++++++++++++++ src/lib/withBaseURL.ts | 2 +- src/machines/authMachine.ts | 55 +++++++++++++++++++++++----------- src/routes/SignIn.tsx | 5 ++-- 13 files changed, 104 insertions(+), 46 deletions(-) create mode 100644 src/lib/withBaseURL.test.ts diff --git a/scripts/known/urls.txt b/scripts/known/urls.txt index 2b08be00e..a4e43158f 100644 --- a/scripts/known/urls.txt +++ b/scripts/known/urls.txt @@ -4,6 +4,8 @@ URL STATUS 000 https://${BASE_URL} +405 https://api.dev.zoo.dev/oauth2/token/revoke +401 https://api.dev.zoo.dev/users 301 https://discord.gg/JQEpHR7Nt2 404 https://github.com/KittyCAD/engine/issues/3528 404 https://github.com/KittyCAD/modeling-app/commit/${ref} diff --git a/src/components/LspProvider.tsx b/src/components/LspProvider.tsx index 5e9155bec..cb3eb6fba 100644 --- a/src/components/LspProvider.tsx +++ b/src/components/LspProvider.tsx @@ -7,7 +7,7 @@ import { LanguageServerClient, LspWorkerEventType, } from '@kittycad/codemirror-lsp-client' -import { TEST, VITE_KC_API_BASE_URL } from '@src/env' +import { TEST } from '@src/env' import React, { createContext, useContext, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import type * as LSP from 'vscode-languageserver-protocol' @@ -28,6 +28,7 @@ import type { FileEntry } from '@src/lib/project' import { codeManager } from '@src/lib/singletons' import { err } from '@src/lib/trap' import { useToken } from '@src/lib/singletons' +import { withAPIBaseURL } from '@src/lib/withBaseURL' function getWorkspaceFolders(): LSP.WorkspaceFolder[] { return [] @@ -85,7 +86,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { const initEvent: KclWorkerOptions = { wasmUrl: wasmUrl(), token: token, - apiBaseUrl: VITE_KC_API_BASE_URL, + apiBaseUrl: withAPIBaseURL(''), } lspWorker.postMessage({ worker: LspWorker.Kcl, @@ -178,7 +179,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { const initEvent: CopilotWorkerOptions = { wasmUrl: wasmUrl(), token: token, - apiBaseUrl: VITE_KC_API_BASE_URL, + apiBaseUrl: withAPIBaseURL(''), } lspWorker.postMessage({ worker: LspWorker.Copilot, diff --git a/src/lib/coredump.ts b/src/lib/coredump.ts index bb88a8637..40040871f 100644 --- a/src/lib/coredump.ts +++ b/src/lib/coredump.ts @@ -1,4 +1,3 @@ -import { VITE_KC_API_BASE_URL } from '@src/env' import { UAParser } from 'ua-parser-js' import type { OsInfo } from '@rust/kcl-lib/bindings/OsInfo' @@ -11,6 +10,7 @@ import { isDesktop } from '@src/lib/isDesktop' import type RustContext from '@src/lib/rustContext' import screenshot from '@src/lib/screenshot' import { APP_VERSION } from '@src/routes/utils' +import { withAPIBaseURL } from '@src/lib/withBaseURL' /* eslint-disable suggest-no-throw/suggest-no-throw -- * All the throws in CoreDumpManager are intentional and should be caught and handled properly @@ -35,7 +35,7 @@ export class CoreDumpManager { codeManager: CodeManager rustContext: RustContext token: string | undefined - baseUrl: string = VITE_KC_API_BASE_URL + baseUrl: string = withAPIBaseURL('') constructor( engineCommandManager: EngineCommandManager, diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index 3cdba649d..cecba01a8 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -26,6 +26,7 @@ import { err } from '@src/lib/trap' import type { DeepPartial } from '@src/lib/types' import { getInVariableCase } from '@src/lib/utils' import { IS_STAGING } from '@src/routes/utils' +import { withAPIBaseURL } from '@src/lib/withBaseURL' export async function renameProjectDirectory( projectPath: string, @@ -697,7 +698,9 @@ export const readTokenFile = async () => { export const writeTokenFile = async (token: string) => { const tokenFilePath = await getTokenFilePath() if (err(token)) return Promise.reject(token) - return window.electron.writeFile(tokenFilePath, token) + const result = window.electron.writeFile(tokenFilePath, token) + console.log('token written to disk') + return result } export const writeTelemetryFile = async (content: string) => { @@ -722,12 +725,9 @@ export const setState = async (state: Project | undefined): Promise => { appStateStore = state } -export const getUser = async ( - token: string, - hostname: string -): Promise => { +export const getUser = async (token: string): Promise => { try { - const user = await fetch(`${hostname}/users/me`, { + const user = await fetch(withAPIBaseURL('/users/me'), { headers: new Headers({ Authorization: `Bearer ${token}`, }), diff --git a/src/lib/links.ts b/src/lib/links.ts index 8c2b0d599..544fbff19 100644 --- a/src/lib/links.ts +++ b/src/lib/links.ts @@ -1,4 +1,4 @@ -import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from '@src/env' +import { VITE_KC_SITE_APP_URL } from '@src/env' import toast from 'react-hot-toast' import { stringToBase64 } from '@src/lib/base64' @@ -7,6 +7,7 @@ import { CREATE_FILE_URL_PARAM, } from '@src/lib/constants' import { err } from '@src/lib/trap' +import { withAPIBaseURL } from '@src/lib/withBaseURL' export interface FileLinkParams { code: string @@ -96,7 +97,7 @@ export async function createShortlink( if (password) { body.password = password } - const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, { + const response = await fetch(withAPIBaseURL('/user/shortlinks'), { method: 'POST', headers: { 'Content-type': 'application/json', diff --git a/src/lib/promptToEdit.tsx b/src/lib/promptToEdit.tsx index 5afe6250a..428bd5162 100644 --- a/src/lib/promptToEdit.tsx +++ b/src/lib/promptToEdit.tsx @@ -1,7 +1,7 @@ import type { SelectionRange } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state' import type { Models } from '@kittycad/lib' -import { VITE_KC_API_BASE_URL, VITE_KC_SITE_BASE_URL } from '@src/env' +import { VITE_KC_SITE_BASE_URL } from '@src/env' import { diffLines } from 'diff' import toast from 'react-hot-toast' import type { TextToCadMultiFileIteration_type } from '@kittycad/lib/dist/types/src/models' @@ -28,6 +28,7 @@ import { uuidv4 } from '@src/lib/utils' import type { File as KittyCadLibFile } from '@kittycad/lib/dist/types/src/models' import type { FileMeta } from '@src/lib/types' import type { RequestedKCLFile } from '@src/machines/systemIO/utils' +import { withAPIBaseURL } from '@src/lib/withBaseURL' type KclFileMetaMap = { [execStateFileNamesIndex: number]: Extract @@ -77,7 +78,7 @@ async function submitTextToCadRequest( }) const response = await fetch( - `${VITE_KC_API_BASE_URL}/ml/text-to-cad/multi-file/iteration`, + withAPIBaseURL('/ml/text-to-cad/multi-file/iteration'), { method: 'POST', headers: { @@ -304,7 +305,7 @@ export async function getPromptToEditResult( id: string, token?: string ): Promise { - const url = VITE_KC_API_BASE_URL + '/async/operations/' + id + const url = withAPIBaseURL(`/async/operations/${id}`) const data: Models['TextToCadMultiFileIteration_type'] | Error = await crossPlatformFetch( url, @@ -340,7 +341,7 @@ export async function doPromptEdit({ ;(window as any).process = { env: { ZOO_API_TOKEN: token, - ZOO_HOST: VITE_KC_API_BASE_URL, + ZOO_HOST: withAPIBaseURL(''), }, } try { diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index 8eb57da67..5202ed83c 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -1,4 +1,4 @@ -import { VITE_KC_API_BASE_URL } from '@src/env' +import { withAPIBaseURL } from '@src/lib/withBaseURL' import EditorManager from '@src/editor/manager' import { KclManager } from '@src/lang/KclSingleton' @@ -171,7 +171,7 @@ const appMachine = setup({ systemId: BILLING, input: { ...BILLING_CONTEXT_DEFAULTS, - urlUserService: VITE_KC_API_BASE_URL, + urlUserService: withAPIBaseURL(''), }, }), ], diff --git a/src/lib/textToCad.ts b/src/lib/textToCad.ts index 7e9f5f4dc..01b0677dc 100644 --- a/src/lib/textToCad.ts +++ b/src/lib/textToCad.ts @@ -1,5 +1,4 @@ import type { Models } from '@kittycad/lib' -import { VITE_KC_API_BASE_URL } from '@src/env' import toast from 'react-hot-toast' import type { NavigateFunction } from 'react-router-dom' import { @@ -19,6 +18,7 @@ import { err, reportRejection } from '@src/lib/trap' import { toSync } from '@src/lib/utils' import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext' import { joinOSPaths } from '@src/lib/paths' +import { withAPIBaseURL } from '@src/lib/withBaseURL' export async function submitTextToCadPrompt( prompt: string, @@ -32,7 +32,7 @@ export async function submitTextToCadPrompt( kcl_version: kclManager.kclVersion, } // Glb has a smaller footprint than gltf, should we want to render it. - const url = VITE_KC_API_BASE_URL + '/ai/text-to-cad/glb?kcl=true' + const url = withAPIBaseURL('/ai/text-to-cad/glb?kcl=true') const data: Models['TextToCad_type'] | Error = await crossPlatformFetch( url, { @@ -58,7 +58,7 @@ export async function getTextToCadResult( id: string, token?: string ): Promise { - const url = VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id + const url = withAPIBaseURL(`/user/text-to-cad/${id}`) const data: Models['TextToCad_type'] | Error = await crossPlatformFetch( url, { diff --git a/src/lib/textToCadTelemetry.ts b/src/lib/textToCadTelemetry.ts index a5c336522..b3d64b0d2 100644 --- a/src/lib/textToCadTelemetry.ts +++ b/src/lib/textToCadTelemetry.ts @@ -1,14 +1,13 @@ import type { Models } from '@kittycad/lib/dist/types/src' -import { VITE_KC_API_BASE_URL } from '@src/env' import crossPlatformFetch from '@src/lib/crossPlatformFetch' +import { withAPIBaseURL } from '@src/lib/withBaseURL' export async function sendTelemetry( id: string, feedback: Models['MlFeedback_type'], token?: string ): Promise { - const url = - VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id + '?feedback=' + feedback + const url = withAPIBaseURL(`/user/text-to-cad/${id}?feedback=${feedback}`) await crossPlatformFetch( url, { diff --git a/src/lib/withBaseURL.test.ts b/src/lib/withBaseURL.test.ts new file mode 100644 index 000000000..f0a631b77 --- /dev/null +++ b/src/lib/withBaseURL.test.ts @@ -0,0 +1,34 @@ +import { withAPIBaseURL } from '@src/lib/withBaseURL' + +describe('withBaseURL', () => { + /** + * running in the development environment + * the .env.development should load + */ + describe('withAPIBaseUrl', () => { + it('should return base url', () => { + const expected = 'https://api.dev.zoo.dev' + const actual = withAPIBaseURL('') + expect(actual).toBe(expected) + }) + it('should return base url with /users', () => { + const expected = 'https://api.dev.zoo.dev/users' + const actual = withAPIBaseURL('/users') + expect(actual).toBe(expected) + }) + it('should return a longer base url with /oauth2/token/revoke', () => { + const expected = 'https://api.dev.zoo.dev/oauth2/token/revoke' + const actual = withAPIBaseURL('/oauth2/token/revoke') + expect(actual).toBe(expected) + }) + it('should ensure base url does not have ending slash', () => { + const expected = 'https://api.dev.zoo.dev' + const actual = withAPIBaseURL('') + expect(actual).toBe(expected) + const expectedEndsWith = expected[expected.length - 1] + const actualEndsWith = actual[actual.length - 1] + expect(actual).toBe(expected) + expect(actualEndsWith).toBe(expectedEndsWith) + }) + }) +}) diff --git a/src/lib/withBaseURL.ts b/src/lib/withBaseURL.ts index e23436bd3..5eccdff66 100644 --- a/src/lib/withBaseURL.ts +++ b/src/lib/withBaseURL.ts @@ -1,5 +1,5 @@ import { VITE_KC_API_BASE_URL } from '@src/env' -export default function withBaseUrl(path: string): string { +export function withAPIBaseURL(path: string): string { return VITE_KC_API_BASE_URL + path } diff --git a/src/machines/authMachine.ts b/src/machines/authMachine.ts index 97acd35a4..fcc8c3486 100644 --- a/src/machines/authMachine.ts +++ b/src/machines/authMachine.ts @@ -1,5 +1,5 @@ import type { Models } from '@kittycad/lib' -import { VITE_KC_API_BASE_URL, VITE_KC_DEV_TOKEN } from '@src/env' +import { VITE_KC_DEV_TOKEN } from '@src/env' import { assign, fromPromise, setup } from 'xstate' import { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants' @@ -10,10 +10,7 @@ import { } from '@src/lib/desktop' import { isDesktop } from '@src/lib/isDesktop' import { markOnce } from '@src/lib/performance' -import { - default as withBaseURL, - default as withBaseUrl, -} from '@src/lib/withBaseURL' +import { withAPIBaseURL } from '@src/lib/withBaseURL' import { ACTOR_IDS } from '@src/machines/machineConstants' export interface UserContext { @@ -31,11 +28,21 @@ export type Events = } export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' + +/** + * Determine which token do we have persisted to initialize the auth machine + */ +const persistedCookie = getCookie(COOKIE_NAME) +const persistedLocalStorage = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' +const persistedDevToken = VITE_KC_DEV_TOKEN export const persistedToken = - VITE_KC_DEV_TOKEN || - getCookie(COOKIE_NAME) || - localStorage?.getItem(TOKEN_PERSIST_KEY) || - '' + persistedDevToken || persistedCookie || persistedLocalStorage +console.log('Initial persisted token') +console.table([ + ['cookie', !!persistedCookie], + ['local storage', !!persistedLocalStorage], + ['api token', !!persistedDevToken], +]) export const authMachine = setup({ types: {} as { @@ -132,7 +139,7 @@ export const authMachine = setup({ async function getUser(input: { token?: string }) { const token = await getAndSyncStoredToken(input) - const url = withBaseURL('/user') + const url = withAPIBaseURL('/user') const headers: { [key: string]: string } = { 'Content-Type': 'application/json', } @@ -141,7 +148,7 @@ async function getUser(input: { token?: string }) { if (token) headers['Authorization'] = `Bearer ${token}` const userPromise = isDesktop() - ? getUserDesktop(token, VITE_KC_API_BASE_URL) + ? getUserDesktop(token) : fetch(url, { method: 'GET', credentials: 'include', @@ -190,12 +197,24 @@ async function getAndSyncStoredToken(input: { token?: string }): Promise { // dev mode - if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN + if (VITE_KC_DEV_TOKEN) { + console.log('Token used for authentication') + console.table([['api token', !!VITE_KC_DEV_TOKEN]]) + return VITE_KC_DEV_TOKEN + } - const token = - input.token && input.token !== '' - ? input.token - : getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || '' + const inputToken = input.token && input.token !== '' ? input.token : '' + const cookieToken = getCookie(COOKIE_NAME) + const localStorageToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' + const token = inputToken || cookieToken || localStorageToken + + console.log('Token used for authentication') + console.table([ + ['persisted token', !!inputToken], + ['cookie', !!cookieToken], + ['local storage', !!localStorageToken], + ['api token', !!VITE_KC_DEV_TOKEN], + ]) if (token) { // has just logged in, update storage localStorage.setItem(TOKEN_PERSIST_KEY, token) @@ -221,7 +240,7 @@ async function logout() { if (token) { try { - await fetch(withBaseUrl('/oauth2/token/revoke'), { + await fetch(withAPIBaseURL('/oauth2/token/revoke'), { method: 'POST', credentials: 'include', headers: { @@ -244,7 +263,7 @@ async function logout() { } } - return fetch(withBaseUrl('/logout'), { + return fetch(withAPIBaseURL('/logout'), { method: 'POST', credentials: 'include', }) diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index 595c16609..6dc269b68 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -6,7 +6,7 @@ import { Link } from 'react-router-dom' import { ActionButton } from '@src/components/ActionButton' import { CustomIcon } from '@src/components/CustomIcon' import { Logo } from '@src/components/Logo' -import { VITE_KC_API_BASE_URL, VITE_KC_SITE_BASE_URL } from '@src/env' +import { VITE_KC_SITE_BASE_URL } from '@src/env' import { APP_NAME } from '@src/lib/constants' import { isDesktop } from '@src/lib/isDesktop' import { openExternalBrowserIfDesktop } from '@src/lib/openWindow' @@ -15,6 +15,7 @@ import { reportRejection } from '@src/lib/trap' import { toSync } from '@src/lib/utils' import { authActor, useSettings } from '@src/lib/singletons' import { APP_VERSION, generateSignInUrl } from '@src/routes/utils' +import { withAPIBaseURL } from '@src/lib/withBaseURL' const subtleBorder = 'border border-solid border-chalkboard-30 dark:border-chalkboard-80' @@ -54,7 +55,7 @@ const SignIn = () => { const signInDesktop = async () => { // We want to invoke our command to login via device auth. const userCodeToDisplay = await window.electron - .startDeviceFlow(VITE_KC_API_BASE_URL + location.search) + .startDeviceFlow(withAPIBaseURL(location.search)) .catch(reportError) if (!userCodeToDisplay) { console.error('No user code received while trying to log in') From a0fe33260eddccfd945d634c17f1f80ec184a049 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 12:15:20 -0400 Subject: [PATCH 10/15] WIP new util function --- src/lang/modifyAst.test.ts | 45 +++++++++ src/lang/modifyAst.ts | 42 ++++++++ src/lang/modifyAst/addSweep.ts | 171 ++++----------------------------- 3 files changed, 105 insertions(+), 153 deletions(-) diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index a420704ad..d0e74b0ef 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -2,7 +2,9 @@ import type { Node } from '@rust/kcl-lib/bindings/Node' import { createArrayExpression, + createCallExpressionStdLibKw, createIdentifier, + createLabeledArg, createLiteral, createLiteralMaybeSuffix, createLocalName, @@ -19,6 +21,7 @@ import { createVariableExpressionsArray, deleteSegmentFromPipeExpression, moveValueIntoNewVariable, + setCallInAst, sketchOnExtrudedFace, splitPipedProfile, } from '@src/lang/modifyAst' @@ -1037,3 +1040,45 @@ extrude001 = extrude(profile001, length = 123) expect(node.node.end).toEqual(startOfKwargIndex + 3) }) }) + +describe('Testing setCallInAst', () => { + it('should push an extrude call with variable on variable profile', () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +` + const ast = assertParse(code) + const exprs = createVariableExpressionsArray([ + createLocalName('profile001'), + ]) + const call = createCallExpressionStdLibKw('extrude', exprs, [ + createLabeledArg('length', createLiteral(5)), + ]) + const pathToNode = setCallInAst(ast, call) + if (err(pathToNode)) { + throw pathToNode + } + const newCode = recast(ast) + expect(newCode).toContain(code) + expect(newCode).toContain(`extrude001 = extrude(profile001, length = 5)`) + }) + + it('should push an extrude call with variable on variable profile', () => { + const code = `startSketchOn(XY) + |> circle(sketch001, center = [0, 0], radius = 1) +` + const ast = assertParse(code) + const exprs = createVariableExpressionsArray([ + createLocalName('profile001'), + ]) + const call = createCallExpressionStdLibKw('extrude', exprs, [ + createLabeledArg('length', createLiteral(5)), + ]) + const pathToNode = setCallInAst(ast, call) + if (err(pathToNode)) { + throw pathToNode + } + const newCode = recast(ast) + expect(newCode).toContain(code) + expect(newCode).toContain(`|> extrude(length = 5)`) + }) +}) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index da64d8d7c..f12d32664 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -1247,3 +1247,45 @@ export function createPathToNodeForLastVariable( return pathToCall } + +export function setCallInAst( + ast: Node, + call: Node, + nodeToEdit?: PathToNode, + lastPathToNode?: PathToNode +): Error | PathToNode { + let pathToNode: PathToNode | undefined + if (nodeToEdit) { + const result = getNodeFromPath( + ast, + nodeToEdit, + 'CallExpressionKw' + ) + if (err(result)) { + return result + } + + Object.assign(result.node, call) + pathToNode = nodeToEdit + } else { + if (!call.unlabeled && lastPathToNode) { + const pipe = getNodeFromPath( + ast, + lastPathToNode, + 'PipeExpression' + ) + if (err(pipe)) { + return pipe + } + pipe.node.body.push(call) + pathToNode = lastPathToNode + } else { + const name = findUniqueName(ast, call.callee.name.name) + const declaration = createVariableDeclaration(name, call) + ast.body.push(declaration) + pathToNode = createPathToNodeForLastVariable(ast) + } + } + + return pathToNode +} diff --git a/src/lang/modifyAst/addSweep.ts b/src/lang/modifyAst/addSweep.ts index 3f39c2c50..00a6cc1a3 100644 --- a/src/lang/modifyAst/addSweep.ts +++ b/src/lang/modifyAst/addSweep.ts @@ -5,13 +5,11 @@ import { createLabeledArg, createLiteral, createLocalName, - createVariableDeclaration, - findUniqueName, } from '@src/lang/create' import { - createPathToNodeForLastVariable, createVariableExpressionsArray, insertVariableAndOffsetPathToNode, + setCallInAst, } from '@src/lang/modifyAst' import { getEdgeTagCall, @@ -23,15 +21,8 @@ import { valueOrVariable, } from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' -import type { - CallExpressionKw, - PathToNode, - PipeExpression, - Program, - VariableDeclaration, -} from '@src/lang/wasm' +import type { PathToNode, Program, VariableDeclaration } from '@src/lang/wasm' import type { KclCommandValue } from '@src/lib/commandTypes' -import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants' import type { Selections } from '@src/lib/selections' import { err } from '@src/lib/trap' @@ -116,42 +107,10 @@ export function addExtrude({ // 3. If edit, we assign the new function call declaration to the existing node, // otherwise just push to the end - let pathToNode: PathToNode | undefined - if (nodeToEdit) { - const result = getNodeFromPath( - modifiedAst, - nodeToEdit, - 'CallExpressionKw' - ) - if (err(result)) { - return result - } - - Object.assign(result.node, call) - pathToNode = nodeToEdit - } else { - const lastPathToNode: PathToNode | undefined = - variableExpressions.paths.pop() - if (sketchesExpr === null && lastPathToNode) { - const pipe = getNodeFromPath( - modifiedAst, - lastPathToNode, - 'PipeExpression' - ) - if (err(pipe)) { - return pipe - } - pipe.node.body.push(call) - pathToNode = lastPathToNode - } else { - const name = findUniqueName( - modifiedAst, - KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE - ) - const declaration = createVariableDeclaration(name, call) - modifiedAst.body.push(declaration) - pathToNode = createPathToNodeForLastVariable(modifiedAst) - } + const lastPath = variableExpressions.paths.pop() // TODO: check if this is correct + const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath) + if (err(pathToNode)) { + return pathToNode } return { @@ -222,41 +181,10 @@ export function addSweep({ // 3. If edit, we assign the new function call declaration to the existing node, // otherwise just push to the end - let pathToNode: PathToNode | undefined - if (nodeToEdit) { - const result = getNodeFromPath( - modifiedAst, - nodeToEdit, - 'CallExpressionKw' - ) - if (err(result)) { - return result - } - - Object.assign(result.node, call) - pathToNode = nodeToEdit - } else { - const lastPathToNode: PathToNode | undefined = variableExprs.paths.pop() - if (sketchesExpr === null && lastPathToNode) { - const pipe = getNodeFromPath( - modifiedAst, - lastPathToNode, - 'PipeExpression' - ) - if (err(pipe)) { - return pipe - } - pipe.node.body.push(call) - pathToNode = lastPathToNode - } else { - const name = findUniqueName( - modifiedAst, - KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP - ) - const declaration = createVariableDeclaration(name, call) - modifiedAst.body.push(declaration) - pathToNode = createPathToNodeForLastVariable(modifiedAst) - } + const lastPath = variableExprs.paths.pop() // TODO: check if this is correct + const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath) + if (err(pathToNode)) { + return pathToNode } return { @@ -312,42 +240,10 @@ export function addLoft({ // 3. If edit, we assign the new function call declaration to the existing node, // otherwise just push to the end - let pathToNode: PathToNode | undefined - if (nodeToEdit) { - const result = getNodeFromPath( - modifiedAst, - nodeToEdit, - 'CallExpressionKw' - ) - if (err(result)) { - return result - } - - Object.assign(result.node, call) - pathToNode = nodeToEdit - } else { - const lastPathToNode: PathToNode | undefined = variableExprs.paths.pop() - if (sketchesExpr === null && lastPathToNode) { - const pipe = getNodeFromPath( - modifiedAst, - lastPathToNode, - 'PipeExpression' - ) - if (err(pipe)) { - return pipe - } - pipe.node.body.push(call) - pathToNode = lastPathToNode - } else { - const name = findUniqueName( - modifiedAst, - KCL_DEFAULT_CONSTANT_PREFIXES.LOFT - ) - const declaration = createVariableDeclaration(name, call) - modifiedAst.body.push(declaration) - const toFirstKwarg = !!vDegree - pathToNode = createPathToNodeForLastVariable(modifiedAst, toFirstKwarg) - } + const lastPath = variableExprs.paths.pop() // TODO: check if this is correct + const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath) + if (err(pathToNode)) { + return pathToNode } return { @@ -447,41 +343,10 @@ export function addRevolve({ // 3. If edit, we assign the new function call declaration to the existing node, // otherwise just push to the end - let pathToNode: PathToNode | undefined - if (nodeToEdit) { - const result = getNodeFromPath( - modifiedAst, - nodeToEdit, - 'CallExpressionKw' - ) - if (err(result)) { - return result - } - - Object.assign(result.node, call) - pathToNode = nodeToEdit - } else { - const lastPathToNode: PathToNode | undefined = variableExprs.paths.pop() - if (sketchesExpr === null && lastPathToNode) { - const pipe = getNodeFromPath( - modifiedAst, - lastPathToNode, - 'PipeExpression' - ) - if (err(pipe)) { - return pipe - } - pipe.node.body.push(call) - pathToNode = lastPathToNode - } else { - const name = findUniqueName( - modifiedAst, - KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE - ) - const declaration = createVariableDeclaration(name, call) - modifiedAst.body.push(declaration) - pathToNode = createPathToNodeForLastVariable(modifiedAst) - } + const lastPath = variableExprs.paths.pop() // TODO: check if this is correct + const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath) + if (err(pathToNode)) { + return pathToNode } return { From ead4c1286b439513e6a2453f0a4be8357dbbcdd4 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 13:41:31 -0400 Subject: [PATCH 11/15] Add other test case --- src/lang/modifyAst.test.ts | 69 ++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index d0e74b0ef..731554462 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -25,7 +25,11 @@ import { sketchOnExtrudedFace, splitPipedProfile, } from '@src/lang/modifyAst' -import { findUsesOfTagInPipe, getNodeFromPath } from '@src/lang/queryAst' +import { + findUsesOfTagInPipe, + getNodeFromPath, + getVariableExprsFromSelection, +} from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import type { Artifact } from '@src/lang/std/artifactGraph' import { codeRefFromRange } from '@src/lang/std/artifactGraph' @@ -37,6 +41,7 @@ import { enginelessExecutor } from '@src/lib/testHelpers' import { err } from '@src/lib/trap' import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection' import { assertNotErr } from '@src/unitTestUtils' +import type { Selections } from '@src/lib/selections' beforeAll(async () => { await initPromise @@ -1062,18 +1067,33 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) expect(newCode).toContain(`extrude001 = extrude(profile001, length = 5)`) }) - it('should push an extrude call with variable on variable profile', () => { + it('should push an extrude call in pipe is selection was in variable-less pipe', async () => { const code = `startSketchOn(XY) - |> circle(sketch001, center = [0, 0], radius = 1) + |> circle(center = [0, 0], radius = 1) ` const ast = assertParse(code) - const exprs = createVariableExpressionsArray([ - createLocalName('profile001'), - ]) + const { artifactGraph } = await enginelessExecutor(ast) + const artifact = artifactGraph.values().find((a) => a.type === 'path') + if (!artifact) { + throw new Error('Artifact not found in the graph') + } + const selections: Selections = { + graphSelections: [ + { + codeRef: artifact.codeRef, + artifact, + }, + ], + otherSelections: [], + } + const variableExprs = getVariableExprsFromSelection(selections, ast) + if (err(variableExprs)) throw variableExprs + const exprs = createVariableExpressionsArray(variableExprs.exprs) const call = createCallExpressionStdLibKw('extrude', exprs, [ createLabeledArg('length', createLiteral(5)), ]) - const pathToNode = setCallInAst(ast, call) + const lastPathToNode = variableExprs.paths.pop() + const pathToNode = setCallInAst(ast, call, undefined, lastPathToNode) if (err(pathToNode)) { throw pathToNode } @@ -1081,4 +1101,39 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) expect(newCode).toContain(code) expect(newCode).toContain(`|> extrude(length = 5)`) }) + + it('should push an extrude call with variable if selection was in variable pipe', async () => { + const code = `profile001 = startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) +` + const ast = assertParse(code) + const { artifactGraph } = await enginelessExecutor(ast) + const artifact = artifactGraph.values().find((a) => a.type === 'path') + if (!artifact) { + throw new Error('Artifact not found in the graph') + } + const selections: Selections = { + graphSelections: [ + { + codeRef: artifact.codeRef, + artifact, + }, + ], + otherSelections: [], + } + const variableExprs = getVariableExprsFromSelection(selections, ast) + if (err(variableExprs)) throw variableExprs + const exprs = createVariableExpressionsArray(variableExprs.exprs) + const call = createCallExpressionStdLibKw('extrude', exprs, [ + createLabeledArg('length', createLiteral(5)), + ]) + const lastPathToNode = variableExprs.paths.pop() + const pathToNode = setCallInAst(ast, call, undefined, lastPathToNode) + if (err(pathToNode)) { + throw pathToNode + } + const newCode = recast(ast) + expect(newCode).toContain(code) + expect(newCode).toContain(`extrude001 = extrude(profile001, length = 5)`) + }) }) From d7914219da20730b08738a06a16e4a85ed210602 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 13:58:10 -0400 Subject: [PATCH 12/15] We going --- src/lang/modifyAst/sweeps.test.ts | 84 +++++++++++++++++++ src/lang/modifyAst/{addSweep.ts => sweeps.ts} | 0 src/machines/modelingMachine.ts | 2 +- 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/lang/modifyAst/sweeps.test.ts rename src/lang/modifyAst/{addSweep.ts => sweeps.ts} (100%) diff --git a/src/lang/modifyAst/sweeps.test.ts b/src/lang/modifyAst/sweeps.test.ts new file mode 100644 index 000000000..dfb56108c --- /dev/null +++ b/src/lang/modifyAst/sweeps.test.ts @@ -0,0 +1,84 @@ +import { assertParse, recast } from '@src/lang/wasm' +import type { Selections } from '@src/lib/selections' +import { enginelessExecutor } from '@src/lib/testHelpers' +import { err } from '@src/lib/trap' +import { addExtrude } from '@src/lang/modifyAst/sweeps' +import { stringToKclExpression } from '@src/lib/kclHelpers' + +async function getAstAndSketchSelections(code: string) { + const ast = assertParse(code) + if (err(ast)) { + throw new Error('Error while parsing code') + } + const { artifactGraph } = await enginelessExecutor(ast) + const artifact = artifactGraph.values().find((a) => a.type === 'path') + if (!artifact) { + throw new Error('Artifact not found in the graph') + } + const sketches: Selections = { + graphSelections: [ + { + codeRef: artifact.codeRef, + artifact, + }, + ], + otherSelections: [], + } + return { ast, sketches } +} + +async function getKclCommandValue(value: string) { + const result = await stringToKclExpression(value) + if (err(result) || 'errors' in result) { + throw new Error(`Couldn't create kcl expression`) + } + + return result +} + +describe('Testing addExtrude', () => { + it('should push an extrude call in pipe if selection was in variable-less pipe', async () => { + const code = `startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + const length = await getKclCommandValue('1') + const result = addExtrude({ ast, sketches, length }) + if (err(result)) { + return { reason: 'Error while adding extrude' } + } + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain(`|> extrude(length = 1)`) + }) + + it('should push a variable extrude call if selection was in variable profile', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + const length = await getKclCommandValue('2') + const result = addExtrude({ ast, sketches, length }) + if (err(result)) { + return { reason: 'Error while adding extrude' } + } + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain(`extrude001 = extrude(profile001, length = 2)`) + }) + + it('should push an extrude call with variable if selection was in variable pipe', async () => { + const code = `profile001 = startSketchOn(XY) + |> circle(center = [0, 0], radius = 1) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + const length = await getKclCommandValue('3') + const result = addExtrude({ ast, sketches, length }) + if (err(result)) { + return { reason: 'Error while adding extrude' } + } + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain(`extrude001 = extrude(profile001, length = 3)`) + }) +}) diff --git a/src/lang/modifyAst/addSweep.ts b/src/lang/modifyAst/sweeps.ts similarity index 100% rename from src/lang/modifyAst/addSweep.ts rename to src/lang/modifyAst/sweeps.ts diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 0e091bd98..8713d7ce5 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -72,7 +72,7 @@ import { addRevolve, addSweep, getAxisExpressionAndIndex, -} from '@src/lang/modifyAst/addSweep' +} from '@src/lang/modifyAst/sweeps' import { applyIntersectFromTargetOperatorSelections, applySubtractFromTargetOperatorSelections, From b955184191506450409a0dcdb525ee235d23f9ee Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 14:31:06 -0400 Subject: [PATCH 13/15] Lint & complete addExtrude tests --- src/lang/modifyAst/sweeps.test.ts | 41 +++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/lang/modifyAst/sweeps.test.ts b/src/lang/modifyAst/sweeps.test.ts index dfb56108c..662f26ad1 100644 --- a/src/lang/modifyAst/sweeps.test.ts +++ b/src/lang/modifyAst/sweeps.test.ts @@ -28,12 +28,12 @@ async function getAstAndSketchSelections(code: string) { } async function getKclCommandValue(value: string) { - const result = await stringToKclExpression(value) - if (err(result) || 'errors' in result) { - throw new Error(`Couldn't create kcl expression`) - } + const result = await stringToKclExpression(value) + if (err(result) || 'errors' in result) { + throw new Error(`Couldn't create kcl expression`) + } - return result + return result } describe('Testing addExtrude', () => { @@ -81,4 +81,35 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) expect(newCode).toContain(code) expect(newCode).toContain(`extrude001 = extrude(profile001, length = 3)`) }) + + it('should push an extrude call with all optional args if asked', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + const length = await getKclCommandValue('10') + const symmetric = true + const bidirectionalLength = await getKclCommandValue('20') + const twistAngle = await getKclCommandValue('30') + const result = addExtrude({ + ast, + sketches, + length, + symmetric, + bidirectionalLength, + twistAngle, + }) + if (err(result)) { + return { reason: 'Error while adding extrude' } + } + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain(`extrude001 = extrude( + profile001, + length = 10, + symmetric = true, + bidirectionalLength = 20, + twistAngle = 30, +)`) + }) }) From 5a4a32c0448fb7e824c2ecf95aa7fbc6de470e81 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 15:00:21 -0400 Subject: [PATCH 14/15] Add addSweep test --- src/lang/modifyAst/sweeps.test.ts | 102 +++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 23 deletions(-) diff --git a/src/lang/modifyAst/sweeps.test.ts b/src/lang/modifyAst/sweeps.test.ts index 662f26ad1..80bf5c6bb 100644 --- a/src/lang/modifyAst/sweeps.test.ts +++ b/src/lang/modifyAst/sweeps.test.ts @@ -1,21 +1,27 @@ -import { assertParse, recast } from '@src/lang/wasm' +import { + type Artifact, + assertParse, + type CodeRef, + recast, +} from '@src/lang/wasm' import type { Selections } from '@src/lib/selections' import { enginelessExecutor } from '@src/lib/testHelpers' import { err } from '@src/lib/trap' -import { addExtrude } from '@src/lang/modifyAst/sweeps' +import { addExtrude, addSweep } from '@src/lang/modifyAst/sweeps' import { stringToKclExpression } from '@src/lib/kclHelpers' -async function getAstAndSketchSelections(code: string) { +async function getAstAndArtifactGraph(code: string) { const ast = assertParse(code) - if (err(ast)) { - throw new Error('Error while parsing code') - } + if (err(ast)) throw ast + const { artifactGraph } = await enginelessExecutor(ast) - const artifact = artifactGraph.values().find((a) => a.type === 'path') - if (!artifact) { - throw new Error('Artifact not found in the graph') - } - const sketches: Selections = { + return { ast, artifactGraph } +} + +function createSelectionFromPathArtifact( + artifact: Artifact & { codeRef: CodeRef } +): Selections { + return { graphSelections: [ { codeRef: artifact.codeRef, @@ -24,6 +30,15 @@ async function getAstAndSketchSelections(code: string) { ], otherSelections: [], } +} + +async function getAstAndSketchSelections(code: string) { + const { ast, artifactGraph } = await getAstAndArtifactGraph(code) + const artifact = artifactGraph.values().find((a) => a.type === 'path') + if (!artifact) { + throw new Error('Artifact not found in the graph') + } + const sketches = createSelectionFromPathArtifact(artifact) return { ast, sketches } } @@ -44,9 +59,7 @@ describe('Testing addExtrude', () => { const { ast, sketches } = await getAstAndSketchSelections(code) const length = await getKclCommandValue('1') const result = addExtrude({ ast, sketches, length }) - if (err(result)) { - return { reason: 'Error while adding extrude' } - } + if (err(result)) throw result const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`|> extrude(length = 1)`) @@ -59,9 +72,7 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) const { ast, sketches } = await getAstAndSketchSelections(code) const length = await getKclCommandValue('2') const result = addExtrude({ ast, sketches, length }) - if (err(result)) { - return { reason: 'Error while adding extrude' } - } + if (err(result)) throw result const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`extrude001 = extrude(profile001, length = 2)`) @@ -74,9 +85,7 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) const { ast, sketches } = await getAstAndSketchSelections(code) const length = await getKclCommandValue('3') const result = addExtrude({ ast, sketches, length }) - if (err(result)) { - return { reason: 'Error while adding extrude' } - } + if (err(result)) throw result const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`extrude001 = extrude(profile001, length = 3)`) @@ -99,9 +108,7 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) bidirectionalLength, twistAngle, }) - if (err(result)) { - return { reason: 'Error while adding extrude' } - } + if (err(result)) throw result const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`extrude001 = extrude( @@ -112,4 +119,53 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) twistAngle = 30, )`) }) + + // TODO: missing edit flow test + + // TODO: missing multi-profile test +}) + +describe('Testing addSweep', () => { + it('should push a sweep call with all optional args if asked', async () => { + const code = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [0, 0], radius = 1) +sketch002 = startSketchOn(XZ) +profile002 = startProfile(sketch002, at = [0, 0]) + |> xLine(length = -5) + |> tangentialArc(endAbsolute = [-20, 5]) +` + const { ast, artifactGraph } = await getAstAndArtifactGraph(code) + const artifact1 = artifactGraph.values().find((a) => a.type === 'path') + const artifact2 = [...artifactGraph.values()].findLast( + (a) => a.type === 'path' + ) + if (!artifact1 || !artifact2) { + throw new Error('Artifact not found in the graph') + } + + const sketches = createSelectionFromPathArtifact(artifact1) + const path = createSelectionFromPathArtifact(artifact2) + const sectional = true + const relativeTo = 'sketchPlane' + const result = addSweep({ + ast, + sketches, + path, + sectional, + relativeTo, + }) + if (err(result)) throw result + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain(`sweep001 = sweep( + profile001, + path = profile002, + sectional = true, + relativeTo = 'sketchPlane', +)`) + }) + + // TODO: missing edit flow test + + // TODO: missing multi-profile test }) From 172e01529c0b3d827cd57e5206c8e186dbadc785 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Jul 2025 15:45:32 -0400 Subject: [PATCH 15/15] Add basic addLoft and addRevolve tests, will have to see how to distribute coverage --- src/lang/modifyAst/sweeps.test.ts | 123 +++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 21 deletions(-) diff --git a/src/lang/modifyAst/sweeps.test.ts b/src/lang/modifyAst/sweeps.test.ts index 80bf5c6bb..87772a9fc 100644 --- a/src/lang/modifyAst/sweeps.test.ts +++ b/src/lang/modifyAst/sweeps.test.ts @@ -2,13 +2,20 @@ import { type Artifact, assertParse, type CodeRef, + type Program, recast, } from '@src/lang/wasm' -import type { Selections } from '@src/lib/selections' +import type { Selection, Selections } from '@src/lib/selections' import { enginelessExecutor } from '@src/lib/testHelpers' import { err } from '@src/lib/trap' -import { addExtrude, addSweep } from '@src/lang/modifyAst/sweeps' +import { + addExtrude, + addLoft, + addRevolve, + addSweep, +} from '@src/lang/modifyAst/sweeps' import { stringToKclExpression } from '@src/lib/kclHelpers' +import type { Node } from '@rust/kcl-lib/bindings/Node' async function getAstAndArtifactGraph(code: string) { const ast = assertParse(code) @@ -19,26 +26,28 @@ async function getAstAndArtifactGraph(code: string) { } function createSelectionFromPathArtifact( - artifact: Artifact & { codeRef: CodeRef } + artifacts: (Artifact & { codeRef: CodeRef })[] ): Selections { - return { - graphSelections: [ - { + const graphSelections = artifacts.map( + (artifact) => + ({ codeRef: artifact.codeRef, artifact, - }, - ], + }) as Selection + ) + return { + graphSelections, otherSelections: [], } } async function getAstAndSketchSelections(code: string) { const { ast, artifactGraph } = await getAstAndArtifactGraph(code) - const artifact = artifactGraph.values().find((a) => a.type === 'path') - if (!artifact) { + const artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path') + if (artifacts.length === 0) { throw new Error('Artifact not found in the graph') } - const sketches = createSelectionFromPathArtifact(artifact) + const sketches = createSelectionFromPathArtifact(artifacts) return { ast, sketches } } @@ -51,8 +60,15 @@ async function getKclCommandValue(value: string) { return result } +async function runNewAstAndCheckForSweep(ast: Node) { + const { artifactGraph } = await enginelessExecutor(ast) + console.log('artifactGraph', artifactGraph) + const sweepArtifact = artifactGraph.values().find((a) => a.type === 'sweep') + expect(sweepArtifact).toBeDefined() +} + describe('Testing addExtrude', () => { - it('should push an extrude call in pipe if selection was in variable-less pipe', async () => { + it('should push a call in pipe if selection was in variable-less pipe', async () => { const code = `startSketchOn(XY) |> circle(center = [0, 0], radius = 1) ` @@ -63,9 +79,10 @@ describe('Testing addExtrude', () => { const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`|> extrude(length = 1)`) + await runNewAstAndCheckForSweep(result.modifiedAst) }) - it('should push a variable extrude call if selection was in variable profile', async () => { + it('should push a call with variable if selection was in variable profile', async () => { const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) ` @@ -76,9 +93,10 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`extrude001 = extrude(profile001, length = 2)`) + await runNewAstAndCheckForSweep(result.modifiedAst) }) - it('should push an extrude call with variable if selection was in variable pipe', async () => { + it('should push a call with variable if selection was in variable pipe', async () => { const code = `profile001 = startSketchOn(XY) |> circle(center = [0, 0], radius = 1) ` @@ -86,35 +104,34 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) const length = await getKclCommandValue('3') const result = addExtrude({ ast, sketches, length }) if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`extrude001 = extrude(profile001, length = 3)`) }) - it('should push an extrude call with all optional args if asked', async () => { + it('should push a call with many compatible optional args if asked', async () => { const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) ` const { ast, sketches } = await getAstAndSketchSelections(code) const length = await getKclCommandValue('10') - const symmetric = true const bidirectionalLength = await getKclCommandValue('20') const twistAngle = await getKclCommandValue('30') const result = addExtrude({ ast, sketches, length, - symmetric, bidirectionalLength, twistAngle, }) if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`extrude001 = extrude( profile001, length = 10, - symmetric = true, bidirectionalLength = 20, twistAngle = 30, )`) @@ -126,7 +143,7 @@ profile001 = circle(sketch001, center = [0, 0], radius = 1) }) describe('Testing addSweep', () => { - it('should push a sweep call with all optional args if asked', async () => { + it('should push a call with variable and all compatible optional args', async () => { const code = `sketch001 = startSketchOn(XY) profile001 = circle(sketch001, center = [0, 0], radius = 1) sketch002 = startSketchOn(XZ) @@ -143,8 +160,8 @@ profile002 = startProfile(sketch002, at = [0, 0]) throw new Error('Artifact not found in the graph') } - const sketches = createSelectionFromPathArtifact(artifact1) - const path = createSelectionFromPathArtifact(artifact2) + const sketches = createSelectionFromPathArtifact([artifact1]) + const path = createSelectionFromPathArtifact([artifact2]) const sectional = true const relativeTo = 'sketchPlane' const result = addSweep({ @@ -155,6 +172,7 @@ profile002 = startProfile(sketch002, at = [0, 0]) relativeTo, }) if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) const newCode = recast(result.modifiedAst) expect(newCode).toContain(code) expect(newCode).toContain(`sweep001 = sweep( @@ -169,3 +187,66 @@ profile002 = startProfile(sketch002, at = [0, 0]) // TODO: missing multi-profile test }) + +describe('Testing addLoft', () => { + it('should push a call with variable and all optional args if asked', async () => { + const code = `sketch001 = startSketchOn(XZ) +profile001 = circle(sketch001, center = [0, 0], radius = 30) +plane001 = offsetPlane(XZ, offset = 50) +sketch002 = startSketchOn(plane001) +profile002 = circle(sketch002, center = [0, 0], radius = 20) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + expect(sketches.graphSelections).toHaveLength(2) + const vDegree = await getKclCommandValue('3') + const result = addLoft({ + ast, + sketches, + vDegree, + }) + if (err(result)) throw result + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain( + `loft001 = loft([profile001, profile002], vDegree = 3)` + ) + // Don't think we can find the artifact here for loft? + }) + + // TODO: missing edit flow test +}) + +describe('Testing addRevolve', () => { + it('should push a call with variable and compatible optional args if asked', async () => { + const code = `sketch001 = startSketchOn(XZ) +profile001 = circle(sketch001, center = [3, 0], radius = 1) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + expect(sketches.graphSelections).toHaveLength(1) + const result = addRevolve({ + ast, + sketches, + angle: await getKclCommandValue('1'), + axisOrEdge: 'Axis', + axis: 'X', + edge: undefined, + symmetric: false, + bidirectionalAngle: await getKclCommandValue('2'), + }) + if (err(result)) throw result + await runNewAstAndCheckForSweep(result.modifiedAst) + const newCode = recast(result.modifiedAst) + console.log(newCode) + expect(newCode).toContain(code) + expect(newCode).toContain(`revolve001 = revolve( + profile001, + angle = 1, + axis = X, + bidirectionalAngle = 2, +)`) + }) + + // TODO: missing edit flow test + + // TODO: missing multi-profile test +})