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/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/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; 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/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index f65d4c6ed..731554462 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -2,9 +2,12 @@ import type { Node } from '@rust/kcl-lib/bindings/Node' import { createArrayExpression, + createCallExpressionStdLibKw, createIdentifier, + createLabeledArg, createLiteral, createLiteralMaybeSuffix, + createLocalName, createObjectExpression, createPipeExpression, createPipeSubstitution, @@ -14,12 +17,19 @@ import { } from '@src/lang/create' import { addSketchTo, + createPathToNodeForLastVariable, + createVariableExpressionsArray, deleteSegmentFromPipeExpression, moveValueIntoNewVariable, + setCallInAst, sketchOnExtrudedFace, splitPipedProfile, } from '@src/lang/modifyAst' -import { findUsesOfTagInPipe } 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' @@ -31,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 @@ -917,3 +928,212 @@ 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') + }) +}) + +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) + }) +}) + +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 in pipe is selection was in variable-less pipe', async () => { + const code = `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(`|> 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)`) + }) +}) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index df999321c..f12d32664 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -1209,3 +1209,83 @@ export function insertVariableAndOffsetPathToNode( } } } + +// Create an array expression for variables, +// or keep it null if all are PipeSubstitutions +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) { + exprs = sketches[0] + } else { + exprs = createArrayExpression(sketches) + } + return exprs +} + +// 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 +} + +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/sweeps.test.ts b/src/lang/modifyAst/sweeps.test.ts new file mode 100644 index 000000000..87772a9fc --- /dev/null +++ b/src/lang/modifyAst/sweeps.test.ts @@ -0,0 +1,252 @@ +import { + type Artifact, + assertParse, + type CodeRef, + type Program, + recast, +} from '@src/lang/wasm' +import type { Selection, Selections } from '@src/lib/selections' +import { enginelessExecutor } from '@src/lib/testHelpers' +import { err } from '@src/lib/trap' +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) + if (err(ast)) throw ast + + const { artifactGraph } = await enginelessExecutor(ast) + return { ast, artifactGraph } +} + +function createSelectionFromPathArtifact( + artifacts: (Artifact & { codeRef: CodeRef })[] +): Selections { + 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 artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path') + if (artifacts.length === 0) { + throw new Error('Artifact not found in the graph') + } + const sketches = createSelectionFromPathArtifact(artifacts) + 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 +} + +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 a 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)) throw result + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain(`|> extrude(length = 1)`) + await runNewAstAndCheckForSweep(result.modifiedAst) + }) + + 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) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + const length = await getKclCommandValue('2') + const result = addExtrude({ ast, sketches, length }) + if (err(result)) throw result + const newCode = recast(result.modifiedAst) + expect(newCode).toContain(code) + expect(newCode).toContain(`extrude001 = extrude(profile001, length = 2)`) + await runNewAstAndCheckForSweep(result.modifiedAst) + }) + + 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) +` + const { ast, sketches } = await getAstAndSketchSelections(code) + 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 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 bidirectionalLength = await getKclCommandValue('20') + const twistAngle = await getKclCommandValue('30') + const result = addExtrude({ + ast, + sketches, + length, + 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, + bidirectionalLength = 20, + twistAngle = 30, +)`) + }) + + // TODO: missing edit flow test + + // TODO: missing multi-profile test +}) + +describe('Testing addSweep', () => { + 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) +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 + await runNewAstAndCheckForSweep(result.modifiedAst) + 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 +}) + +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 +}) diff --git a/src/lang/modifyAst/addSweep.ts b/src/lang/modifyAst/sweeps.ts similarity index 69% rename from src/lang/modifyAst/addSweep.ts rename to src/lang/modifyAst/sweeps.ts index ba53bad50..00a6cc1a3 100644 --- a/src/lang/modifyAst/addSweep.ts +++ b/src/lang/modifyAst/sweeps.ts @@ -5,10 +5,12 @@ import { createLabeledArg, createLiteral, createLocalName, - createVariableDeclaration, - findUniqueName, } from '@src/lang/create' -import { insertVariableAndOffsetPathToNode } from '@src/lang/modifyAst' +import { + createVariableExpressionsArray, + insertVariableAndOffsetPathToNode, + setCallInAst, +} from '@src/lang/modifyAst' import { getEdgeTagCall, mutateAstWithTagForSketchSegment, @@ -16,20 +18,11 @@ import { import { getNodeFromPath, getVariableExprsFromSelection, - createVariableExpressionsArray, - createPathToNodeForLastVariable, 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' @@ -114,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 { @@ -220,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 { @@ -310,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 { @@ -445,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 { diff --git a/src/lang/queryAst.test.ts b/src/lang/queryAst.test.ts index 94d570af7..2039b7ea1 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 substitution 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 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) +` + 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', ''], + ]) + }) +}) diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index 904d739c8..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' @@ -1079,16 +1075,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) { @@ -1098,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. 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/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 37df33bef..a02e9cfb3 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, 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 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')