From 9001006823e769916d5ef4d6e246d0bfbfcc5f0a Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Wed, 2 Apr 2025 17:04:24 -0400 Subject: [PATCH 01/28] WIP: Add point-and-click Import for geometry Will eventually fix #6120 Right now the whole loop is there but the codemod doesn't work yet --- src/components/CustomIcon.tsx | 15 ++++++ .../ModelingSidebar/ModelingSidebar.tsx | 12 +++++ src/lang/create.ts | 39 +++++++++++++++ src/lang/modifyAst.ts | 43 +++++++++++++++++ .../modelingCommandConfig.ts | 19 ++++++++ src/machines/modelingMachine.ts | 47 +++++++++++++++++++ 6 files changed, 175 insertions(+) diff --git a/src/components/CustomIcon.tsx b/src/components/CustomIcon.tsx index b3179c9bf80..9d1f35d5578 100644 --- a/src/components/CustomIcon.tsx +++ b/src/components/CustomIcon.tsx @@ -493,6 +493,21 @@ const CustomIconMap = { /> ), + floppyDiskArrowIn: ( + + + + ), folder: ( + commandBarActor.send({ + type: 'Find and select command', + data: { name: 'Import', groupId: 'modeling' }, + }), + }, { id: 'export', title: 'Export part', diff --git a/src/lang/create.ts b/src/lang/create.ts index 1a630d11a35..cc3abf3380f 100644 --- a/src/lang/create.ts +++ b/src/lang/create.ts @@ -1,7 +1,11 @@ +import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement' import type { Name } from '@rust/kcl-lib/bindings/Name' import type { Node } from '@rust/kcl-lib/bindings/Node' import type { TagDeclarator } from '@rust/kcl-lib/bindings/TagDeclarator' +import { ImportPath } from '@rust/kcl-lib/bindings/ImportPath' +import { ImportSelector } from '@rust/kcl-lib/bindings/ImportSelector' +import { ItemVisibility } from '@rust/kcl-lib/bindings/ItemVisibility' import { ARG_TAG } from '@src/lang/constants' import { getNodeFromPath } from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' @@ -12,6 +16,7 @@ import type { CallExpression, CallExpressionKw, Expr, + ExpressionStatement, Identifier, LabeledArg, Literal, @@ -333,6 +338,40 @@ export function createBinaryExpressionWithUnary([left, right]: [ return createBinaryExpression([left, '+', right]) } +export function createImportStatement( + selector: ImportSelector, + path: ImportPath, + visibility: ItemVisibility +): Node { + return { + type: 'ImportStatement', + start: 0, + end: 0, + moduleId: 0, + outerAttrs: [], + preComments: [], + commentStart: 0, + selector, + path, + visibility, + } +} + +export function createExpressionStatement( + expression: Expr +): Node { + return { + type: 'ExpressionStatement', + start: 0, + end: 0, + moduleId: 0, + outerAttrs: [], + preComments: [], + commentStart: 0, + expression, + } +} + export function findUniqueName( ast: Program | string, name: string, diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index f23ad90b59d..5fb1d9ca8dc 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -9,7 +9,9 @@ import { createCallExpression, createCallExpressionStdLib, createCallExpressionStdLibKw, + createExpressionStatement, createIdentifier, + createImportStatement, createLabeledArg, createLiteral, createLocalName, @@ -778,6 +780,47 @@ export function addOffsetPlane({ } } +/** + * Add an import call to load a part + */ +export function addImport({ + node, + path, + localName, +}: { + node: Node + path: string + localName: string +}): { modifiedAst: Node; pathToNode: PathToNode } { + const modifiedAst = structuredClone(node) + const importStatement = createImportStatement( + { type: 'None', alias: createIdentifier(localName) }, + { type: 'Kcl', filename: path }, + 'default' + ) + const expressionStatement = createExpressionStatement( + createLocalName(localName) + ) + const insertAt = modifiedAst.body.length ? modifiedAst.body.length : 0 + modifiedAst.body.push(importStatement) + modifiedAst.body.push(expressionStatement) + const argIndex = 0 + const pathToNode: PathToNode = [ + ['body', ''], + [insertAt, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', 'VariableDeclarator'], + ['arguments', 'CallExpressionKw'], + [argIndex, ARG_INDEX_FIELD], + ['arg', LABELED_ARG_FIELD], + ] + + return { + modifiedAst, + pathToNode, + } +} + /** * Append a helix to the AST */ diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index 00f11598136..fa395958789 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -48,6 +48,10 @@ export type HelixModes = 'Axis' | 'Edge' | 'Cylinder' export type ModelingCommandSchema = { 'Enter sketch': { forceNewSketch?: boolean } + Import: { + path: string + localName: string + } Export: { type: OutputTypeKey storage?: StorageUnion @@ -224,6 +228,21 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, }, ], + Import: { + description: 'Import a part from the current project directory', + icon: 'floppyDiskArrowIn', + needsReview: true, + args: { + path: { + inputType: 'string', + required: true, + }, + localName: { + inputType: 'string', + required: true, + }, + }, + }, Export: { description: 'Export the current model.', icon: 'floppyDiskArrow', diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index bc9ac3bc938..a0ff67ff9ec 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -46,6 +46,7 @@ import { createLiteral, createLocalName } from '@src/lang/create' import { updateModelingState } from '@src/lang/modelingWorkflows' import { addHelix, + addImport, addOffsetPlane, addShell, addSweep, @@ -338,6 +339,7 @@ export type ModelingMachineEvent = data: ModelingCommandSchema['event.parameter.edit'] } | { type: 'Export'; data: ModelingCommandSchema['Export'] } + | { type: 'Import'; data: ModelingCommandSchema['Import'] } | { type: 'Boolean Subtract' data: ModelingCommandSchema['Boolean Subtract'] @@ -2686,6 +2688,33 @@ export const modelingMachine = setup({ ) } ), + importAstMod: fromPromise( + async ({ input }: { input?: ModelingCommandSchema['Import'] }) => { + if (!input) { + return new Error('No input provided') + } + + const ast = kclManager.ast + const { path, localName } = input + const { modifiedAst, pathToNode } = addImport({ + node: ast, + path, + localName, + }) + await updateModelingState( + modifiedAst, + EXECUTION_TYPE_REAL, + { + kclManager, + editorManager, + codeManager, + }, + { + focusPath: [pathToNode], + } + ) + } + ), exportFromEngine: fromPromise( async ({}: { input?: ModelingCommandSchema['Export'] }) => { return undefined as Error | undefined @@ -2834,6 +2863,11 @@ export const modelingMachine = setup({ target: '#Modeling.parameter.editing', }, + Import: { + target: 'Importing', + reenter: true, + }, + Export: { target: 'Exporting', guard: 'Has exportable geometry', @@ -4245,6 +4279,19 @@ export const modelingMachine = setup({ }, }, + Importing: { + invoke: { + src: 'importAstMod', + id: 'importAstMod', + input: ({ event }) => { + if (event.type !== 'Import') return undefined + return event.data + }, + onDone: ['idle'], + onError: ['idle'], + }, + }, + Exporting: { invoke: { src: 'exportFromEngine', From 3afcb330382de921ae1fe8e68927947844e73f3d Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Apr 2025 07:29:36 -0400 Subject: [PATCH 02/28] Better pathToNOde, log on non-working cm dispatch call --- src/lang/codeManager.ts | 6 ++++++ src/lang/modifyAst.ts | 8 ++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lang/codeManager.ts b/src/lang/codeManager.ts index f8bf489dfcc..caf9dc58648 100644 --- a/src/lang/codeManager.ts +++ b/src/lang/codeManager.ts @@ -108,6 +108,11 @@ export default class CodeManager { if (clearHistory) { clearCodeMirrorHistory(editorManager.editorView) } + console.log('what we send to dispatch', { + from: 0, + to: editorManager.editorView.state.doc.length, + insert: code, + }) editorManager.editorView.dispatch({ changes: { from: 0, @@ -119,6 +124,7 @@ export default class CodeManager { Transaction.addToHistory.of(!clearHistory), ], }) + console.log('after dispatch, not reached') } } diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 5fb1d9ca8dc..0aef9d3dd28 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -804,15 +804,11 @@ export function addImport({ const insertAt = modifiedAst.body.length ? modifiedAst.body.length : 0 modifiedAst.body.push(importStatement) modifiedAst.body.push(expressionStatement) - const argIndex = 0 + // TODO: figure out if we send back the module import or the expression const pathToNode: PathToNode = [ ['body', ''], [insertAt, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', 'VariableDeclarator'], - ['arguments', 'CallExpressionKw'], - [argIndex, ARG_INDEX_FIELD], - ['arg', LABELED_ARG_FIELD], + ['path', 'ImportStatement'], ] return { From e00f6c24025d866845ba95ee142e5cfc578b3349 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Apr 2025 12:02:10 -0400 Subject: [PATCH 03/28] Add workaround to updateModelingState not working --- src/lang/modifyAst.ts | 5 ++--- src/machines/modelingMachine.ts | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 0aef9d3dd28..717209fb5c3 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -801,13 +801,12 @@ export function addImport({ const expressionStatement = createExpressionStatement( createLocalName(localName) ) - const insertAt = modifiedAst.body.length ? modifiedAst.body.length : 0 - modifiedAst.body.push(importStatement) + modifiedAst.body.unshift(importStatement) modifiedAst.body.push(expressionStatement) // TODO: figure out if we send back the module import or the expression const pathToNode: PathToNode = [ ['body', ''], - [insertAt, 'index'], + [0, 'index'], ['path', 'ImportStatement'], ] diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index a0ff67ff9ec..fff240e89bd 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -2696,23 +2696,21 @@ export const modelingMachine = setup({ const ast = kclManager.ast const { path, localName } = input - const { modifiedAst, pathToNode } = addImport({ + const { modifiedAst } = addImport({ node: ast, path, localName, }) - await updateModelingState( - modifiedAst, - EXECUTION_TYPE_REAL, - { - kclManager, - editorManager, - codeManager, - }, - { - focusPath: [pathToNode], - } - ) + // TODO: add back updateModelingState + try { + await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst) + await kclManager.executeAst({ + ast: modifiedAst, + }) + } catch (e) { + console.error(e) + toast.error('Error executing import codemod') + } } ), exportFromEngine: fromPromise( From 2bae5fff55565bbd7787798ceda9e3330bbe6d7a Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Apr 2025 12:42:26 -0400 Subject: [PATCH 04/28] Back to updateModelingState with a skip flag --- src/lang/modelingWorkflows.ts | 21 +++++++++++++++------ src/machines/modelingMachine.ts | 16 ++++++---------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/lang/modelingWorkflows.ts b/src/lang/modelingWorkflows.ts index a422a5eb52c..c0e1e5d6dd7 100644 --- a/src/lang/modelingWorkflows.ts +++ b/src/lang/modelingWorkflows.ts @@ -17,6 +17,7 @@ import { EXECUTION_TYPE_NONE, EXECUTION_TYPE_REAL, } from '@src/lib/constants' +import { Selections } from '@src/lib/selections' /** * Updates the complete modeling state: @@ -57,15 +58,23 @@ export async function updateModelingState( range: SourceRange type: string } + skipUpdateAst?: boolean } ): Promise { + let updatedAst: { + newAst: Node + selections?: Selections + } = { newAst: ast } // Step 1: Update AST without executing (prepare selections) - const updatedAst = await dependencies.kclManager.updateAst( - ast, - // false == mock execution. Is this what we want? - false, // Execution handled separately for error resilience - options - ) + // TODO: understand why this skip flag is needed for importAstMod + if (!options?.skipUpdateAst) { + updatedAst = await dependencies.kclManager.updateAst( + ast, + // false == mock execution. Is this what we want? + false, // Execution handled separately for error resilience + options + ) + } // Step 2: Update the code editor and save file await dependencies.codeManager.updateEditorWithAstAndWriteToFile( diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index fff240e89bd..ec47c45622b 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -2701,16 +2701,12 @@ export const modelingMachine = setup({ path, localName, }) - // TODO: add back updateModelingState - try { - await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst) - await kclManager.executeAst({ - ast: modifiedAst, - }) - } catch (e) { - console.error(e) - toast.error('Error executing import codemod') - } + await updateModelingState( + modifiedAst, + EXECUTION_TYPE_REAL, + { kclManager, editorManager, codeManager }, + { skipUpdateAst: true } + ) } ), exportFromEngine: fromPromise( From 79fd4359c2155a34e7b1fae87f0c377f0d4f0c54 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Apr 2025 12:45:07 -0400 Subject: [PATCH 05/28] Better todo --- src/lang/modelingWorkflows.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lang/modelingWorkflows.ts b/src/lang/modelingWorkflows.ts index c0e1e5d6dd7..36018b4211a 100644 --- a/src/lang/modelingWorkflows.ts +++ b/src/lang/modelingWorkflows.ts @@ -65,9 +65,10 @@ export async function updateModelingState( newAst: Node selections?: Selections } = { newAst: ast } - // Step 1: Update AST without executing (prepare selections) - // TODO: understand why this skip flag is needed for importAstMod + // TODO: understand why this skip flag is needed for importAstMod. + // It's unclear why we double casts the AST if (!options?.skipUpdateAst) { + // Step 1: Update AST without executing (prepare selections) updatedAst = await dependencies.kclManager.updateAst( ast, // false == mock execution. Is this what we want? From 1bb37fcec7287bff3a4087146eb061ddeb7a8e1e Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Apr 2025 15:01:15 -0400 Subject: [PATCH 06/28] Change working from Import to Insert, cleanups --- src/components/CustomIcon.tsx | 15 ------- .../ModelingSidebar/ModelingSidebar.tsx | 10 ++--- src/lang/codeManager.ts | 6 --- src/lang/create.ts | 6 ++- src/lang/modelingWorkflows.ts | 2 +- src/lang/modifyAst.ts | 41 +++++++++++++------ .../modelingCommandConfig.ts | 8 ++-- src/machines/modelingMachine.ts | 36 ++++++++-------- 8 files changed, 63 insertions(+), 61 deletions(-) diff --git a/src/components/CustomIcon.tsx b/src/components/CustomIcon.tsx index 9d1f35d5578..b3179c9bf80 100644 --- a/src/components/CustomIcon.tsx +++ b/src/components/CustomIcon.tsx @@ -493,21 +493,6 @@ const CustomIconMap = { /> ), - floppyDiskArrowIn: ( - - - - ), folder: ( commandBarActor.send({ type: 'Find and select command', - data: { name: 'Import', groupId: 'modeling' }, + data: { name: 'Insert', groupId: 'modeling' }, }), }, { diff --git a/src/lang/codeManager.ts b/src/lang/codeManager.ts index caf9dc58648..f8bf489dfcc 100644 --- a/src/lang/codeManager.ts +++ b/src/lang/codeManager.ts @@ -108,11 +108,6 @@ export default class CodeManager { if (clearHistory) { clearCodeMirrorHistory(editorManager.editorView) } - console.log('what we send to dispatch', { - from: 0, - to: editorManager.editorView.state.doc.length, - insert: code, - }) editorManager.editorView.dispatch({ changes: { from: 0, @@ -124,7 +119,6 @@ export default class CodeManager { Transaction.addToHistory.of(!clearHistory), ], }) - console.log('after dispatch, not reached') } } diff --git a/src/lang/create.ts b/src/lang/create.ts index cc3abf3380f..706f19c5e66 100644 --- a/src/lang/create.ts +++ b/src/lang/create.ts @@ -338,10 +338,14 @@ export function createBinaryExpressionWithUnary([left, right]: [ return createBinaryExpression([left, '+', right]) } +export function createImportAsSelector(name: string): ImportSelector { + return { type: 'None', alias: createIdentifier(name) } +} + export function createImportStatement( selector: ImportSelector, path: ImportPath, - visibility: ItemVisibility + visibility: ItemVisibility = 'default' ): Node { return { type: 'ImportStatement', diff --git a/src/lang/modelingWorkflows.ts b/src/lang/modelingWorkflows.ts index aa7898a3d30..dc60ed4574c 100644 --- a/src/lang/modelingWorkflows.ts +++ b/src/lang/modelingWorkflows.ts @@ -60,7 +60,7 @@ export async function updateModelingState( newAst: Node selections?: Selections } = { newAst: ast } - // TODO: understand why this skip flag is needed for importAstMod. + // TODO: understand why this skip flag is needed for insertAstMod. // It's unclear why we double casts the AST if (!options?.skipUpdateAst) { // Step 1: Update AST without executing (prepare selections) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 5d376ca02ab..60e291d35e9 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -11,6 +11,7 @@ import { createCallExpressionStdLibKw, createExpressionStatement, createIdentifier, + createImportAsSelector, createImportStatement, createLabeledArg, createLiteral, @@ -783,7 +784,7 @@ export function addOffsetPlane({ /** * Add an import call to load a part */ -export function addImport({ +export function addImportAndInsert({ node, path, localName, @@ -791,28 +792,42 @@ export function addImport({ node: Node path: string localName: string -}): { modifiedAst: Node; pathToNode: PathToNode } { +}): { + modifiedAst: Node + pathToImportNode: PathToNode + pathToInsertNode: PathToNode +} { const modifiedAst = structuredClone(node) + + // Add import statement + // TODO: add it to the end of existing imports, only if it doesn't exist const importStatement = createImportStatement( - { type: 'None', alias: createIdentifier(localName) }, - { type: 'Kcl', filename: path }, - 'default' - ) - const expressionStatement = createExpressionStatement( - createLocalName(localName) + createImportAsSelector(localName), + { type: 'Kcl', filename: path } ) + const importIndex = 0 modifiedAst.body.unshift(importStatement) - modifiedAst.body.push(expressionStatement) - // TODO: figure out if we send back the module import or the expression - const pathToNode: PathToNode = [ + const pathToImportNode: PathToNode = [ ['body', ''], - [0, 'index'], + [importIndex, 'index'], ['path', 'ImportStatement'], ] + // Add insert statement + // TODO: check that pushing to the end of the body is good here + const insertStatement = createExpressionStatement(createLocalName(localName)) + const insertIndex = modifiedAst.body.length + modifiedAst.body.push(insertStatement) + const pathToInsertNode: PathToNode = [ + ['body', ''], + [insertIndex, 'index'], + ['expression', 'ExpressionStatement'], + ] + return { modifiedAst, - pathToNode, + pathToImportNode, + pathToInsertNode, } } diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index e176d57a9fa..3e7f0ec0294 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -48,7 +48,7 @@ export type HelixModes = 'Axis' | 'Edge' | 'Cylinder' export type ModelingCommandSchema = { 'Enter sketch': { forceNewSketch?: boolean } - Import: { + Insert: { path: string localName: string } @@ -228,9 +228,9 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, }, ], - Import: { - description: 'Import a part from the current project directory', - icon: 'floppyDiskArrowIn', + Insert: { + description: 'Insert from a file in the current project directory', + icon: 'import', needsReview: true, args: { path: { diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 9ef53c9b8b6..30c33e12bb1 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -46,7 +46,7 @@ import { createLiteral, createLocalName } from '@src/lang/create' import { updateModelingState } from '@src/lang/modelingWorkflows' import { addHelix, - addImport, + addImportAndInsert, addOffsetPlane, addShell, addSweep, @@ -339,7 +339,7 @@ export type ModelingMachineEvent = data: ModelingCommandSchema['event.parameter.edit'] } | { type: 'Export'; data: ModelingCommandSchema['Export'] } - | { type: 'Import'; data: ModelingCommandSchema['Import'] } + | { type: 'Insert'; data: ModelingCommandSchema['Insert'] } | { type: 'Boolean Subtract' data: ModelingCommandSchema['Boolean Subtract'] @@ -2678,24 +2678,28 @@ export const modelingMachine = setup({ ) } ), - importAstMod: fromPromise( - async ({ input }: { input?: ModelingCommandSchema['Import'] }) => { + insertAstMod: fromPromise( + async ({ input }: { input?: ModelingCommandSchema['Insert'] }) => { if (!input) { return new Error('No input provided') } const ast = kclManager.ast const { path, localName } = input - const { modifiedAst } = addImport({ - node: ast, - path, - localName, - }) + const { modifiedAst, pathToImportNode, pathToInsertNode } = + addImportAndInsert({ + node: ast, + path, + localName, + }) await updateModelingState( modifiedAst, EXECUTION_TYPE_REAL, { kclManager, editorManager, codeManager }, - { skipUpdateAst: true } + { + skipUpdateAst: true, + focusPath: [pathToImportNode, pathToInsertNode], + } ) } ), @@ -2847,8 +2851,8 @@ export const modelingMachine = setup({ target: '#Modeling.parameter.editing', }, - Import: { - target: 'Importing', + Insert: { + target: 'Inserting', reenter: true, }, @@ -4263,12 +4267,12 @@ export const modelingMachine = setup({ }, }, - Importing: { + Inserting: { invoke: { - src: 'importAstMod', - id: 'importAstMod', + src: 'insertAstMod', + id: 'insertAstMod', input: ({ event }) => { - if (event.type !== 'Import') return undefined + if (event.type !== 'Insert') return undefined return event.data }, onDone: ['idle'], From 9371860f79faf2a392994ee10c319cb256494352 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Thu, 3 Apr 2025 16:59:38 -0400 Subject: [PATCH 07/28] Sister command in kclCommands to populate file options --- .../ModelingSidebar/ModelingSidebar.tsx | 2 +- .../modelingCommandConfig.ts | 3 ++ src/lib/kclCommands.ts | 47 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/components/ModelingSidebar/ModelingSidebar.tsx b/src/components/ModelingSidebar/ModelingSidebar.tsx index 5623f043075..41a6032885b 100644 --- a/src/components/ModelingSidebar/ModelingSidebar.tsx +++ b/src/components/ModelingSidebar/ModelingSidebar.tsx @@ -69,7 +69,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { action: () => commandBarActor.send({ type: 'Find and select command', - data: { name: 'Insert', groupId: 'modeling' }, + data: { name: 'Insert', groupId: 'code' }, }), }, { diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index 3e7f0ec0294..bf79fccc004 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -229,13 +229,16 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, ], Insert: { + // TODO: find a way to populate the options from here instead of the code.Insert sibbling hack description: 'Insert from a file in the current project directory', icon: 'import', needsReview: true, + hide: 'web', args: { path: { inputType: 'string', required: true, + skip: true, }, localName: { inputType: 'string', diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index 41afa552481..fc3812df899 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -19,6 +19,7 @@ import { baseUnitsUnion } from '@src/lib/settings/settingsTypes' import { codeManager, kclManager } from '@src/lib/singletons' import { err, reportRejection } from '@src/lib/trap' import type { IndexLoaderData } from '@src/lib/types' +import { commandBarActor } from '@src/machines/commandBarMachine' interface OnSubmitProps { sampleName: string @@ -96,6 +97,52 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { } }, }, + { + name: 'Insert', + description: 'Insert from a file in the current project directory', + icon: 'import', + needsReview: false, + groupId: 'code', + hide: 'web', + args: { + path: { + inputType: 'options', + required: true, + options: () => { + const projectPath = commandProps.projectData.project?.path + if (!projectPath) { + return [] + } + console.log('projectPath', projectPath) + console.log('children', commandProps.projectData.project?.children) + return Object.values( + commandProps.projectData.project?.children ?? [] + ).map((v) => { + // TODO: actually traverse this properly + const relativePath = v.path + .replace(projectPath, '') + .replace(window.electron.sep, '') + return { + name: relativePath, + value: relativePath, + } + }) + }, + }, + }, + onSubmit: (data) => { + commandBarActor.send({ + type: 'Find and select command', + data: { + name: 'Insert', + groupId: 'modeling', + argDefaultValues: { + path: data?.path, + }, + }, + }) + }, + }, { name: 'format-code', displayName: 'Format Code', From 9e32ce380ae3696fc23e4d15b8a6d31998a07e2b Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Fri, 4 Apr 2025 00:09:00 -0400 Subject: [PATCH 08/28] Improve path selector --- src/components/FileMachineProvider.tsx | 19 ++++++++++++++++++- src/lang/create.ts | 6 +++--- src/lang/modelingWorkflows.ts | 2 +- src/lib/kclCommands.ts | 24 ++++-------------------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index ed77e695774..f2058646cc3 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -446,10 +446,27 @@ export const FileMachineProvider = ({ name: sample.title, })), }, + specialPropsForInsertCommand: { + providedOptions: (project?.children ?? []).flatMap((v) => { + // TODO: add support for full tree traversal when KCL support subdir imports + const relativeFilePath = v.path.replace( + project?.path + window.electron.sep, + '' + ) + const isDirectory = v.children + const isCurrentFile = v.path === file?.path + return isDirectory || isCurrentFile + ? [] + : { + name: relativeFilePath, + value: relativeFilePath, + } + }), + }, }).filter( (command) => kclSamples.length || command.name !== 'open-kcl-example' ), - [codeManager, kclManager, send, kclSamples] + [codeManager, kclManager, send, kclSamples, project, file] ) useEffect(() => { diff --git a/src/lang/create.ts b/src/lang/create.ts index 706f19c5e66..59a36f0c9ee 100644 --- a/src/lang/create.ts +++ b/src/lang/create.ts @@ -3,9 +3,9 @@ import type { Name } from '@rust/kcl-lib/bindings/Name' import type { Node } from '@rust/kcl-lib/bindings/Node' import type { TagDeclarator } from '@rust/kcl-lib/bindings/TagDeclarator' -import { ImportPath } from '@rust/kcl-lib/bindings/ImportPath' -import { ImportSelector } from '@rust/kcl-lib/bindings/ImportSelector' -import { ItemVisibility } from '@rust/kcl-lib/bindings/ItemVisibility' +import type { ImportPath } from '@rust/kcl-lib/bindings/ImportPath' +import type { ImportSelector } from '@rust/kcl-lib/bindings/ImportSelector' +import type { ItemVisibility } from '@rust/kcl-lib/bindings/ItemVisibility' import { ARG_TAG } from '@src/lang/constants' import { getNodeFromPath } from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' diff --git a/src/lang/modelingWorkflows.ts b/src/lang/modelingWorkflows.ts index e56c67d6257..556d512fb26 100644 --- a/src/lang/modelingWorkflows.ts +++ b/src/lang/modelingWorkflows.ts @@ -17,7 +17,7 @@ import { EXECUTION_TYPE_NONE, EXECUTION_TYPE_REAL, } from '@src/lib/constants' -import { Selections } from '@src/lib/selections' +import type { Selections } from '@src/lib/selections' /** * Updates the complete modeling state: diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index fc3812df899..e7fb57f555c 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -35,6 +35,9 @@ interface KclCommandConfig { onSubmit: (p: OnSubmitProps) => Promise providedOptions: CommandArgumentOption[] } + specialPropsForInsertCommand: { + providedOptions: CommandArgumentOption[] + } projectData: IndexLoaderData authToken: string settings: { @@ -108,26 +111,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { path: { inputType: 'options', required: true, - options: () => { - const projectPath = commandProps.projectData.project?.path - if (!projectPath) { - return [] - } - console.log('projectPath', projectPath) - console.log('children', commandProps.projectData.project?.children) - return Object.values( - commandProps.projectData.project?.children ?? [] - ).map((v) => { - // TODO: actually traverse this properly - const relativePath = v.path - .replace(projectPath, '') - .replace(window.electron.sep, '') - return { - name: relativePath, - value: relativePath, - } - }) - }, + options: commandProps.specialPropsForInsertCommand.providedOptions, }, }, onSubmit: (data) => { From 7b7d8b4efd513f6a796821216e8ea71afa60cf71 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Fri, 4 Apr 2025 09:33:50 -0400 Subject: [PATCH 09/28] Unsure: move importAstMod to kclCommands onSubmit :no_mouth: --- .../modelingCommandConfig.ts | 22 --------- src/lib/kclCommands.ts | 43 ++++++++++++------ src/machines/modelingMachine.ts | 45 ------------------- 3 files changed, 30 insertions(+), 80 deletions(-) diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index bf79fccc004..c586d061dde 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -48,10 +48,6 @@ export type HelixModes = 'Axis' | 'Edge' | 'Cylinder' export type ModelingCommandSchema = { 'Enter sketch': { forceNewSketch?: boolean } - Insert: { - path: string - localName: string - } Export: { type: OutputTypeKey storage?: StorageUnion @@ -228,24 +224,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, }, ], - Insert: { - // TODO: find a way to populate the options from here instead of the code.Insert sibbling hack - description: 'Insert from a file in the current project directory', - icon: 'import', - needsReview: true, - hide: 'web', - args: { - path: { - inputType: 'string', - required: true, - skip: true, - }, - localName: { - inputType: 'string', - required: true, - }, - }, - }, Export: { description: 'Export the current model.', icon: 'floppyDiskArrow', diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index e7fb57f555c..d125c7bdd8f 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -2,6 +2,8 @@ import type { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import toast from 'react-hot-toast' import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning' +import { updateModelingState } from '@src/lang/modelingWorkflows' +import { addImportAndInsert } from '@src/lang/modifyAst' import { changeKclSettings, unitAngleToUnitAng, @@ -11,15 +13,15 @@ import type { Command, CommandArgumentOption } from '@src/lib/commandTypes' import { DEFAULT_DEFAULT_ANGLE_UNIT, DEFAULT_DEFAULT_LENGTH_UNIT, + EXECUTION_TYPE_REAL, FILE_EXT, } from '@src/lib/constants' import { isDesktop } from '@src/lib/isDesktop' import { copyFileShareLink } from '@src/lib/links' import { baseUnitsUnion } from '@src/lib/settings/settingsTypes' -import { codeManager, kclManager } from '@src/lib/singletons' +import { codeManager, editorManager, kclManager } from '@src/lib/singletons' import { err, reportRejection } from '@src/lib/trap' import type { IndexLoaderData } from '@src/lib/types' -import { commandBarActor } from '@src/machines/commandBarMachine' interface OnSubmitProps { sampleName: string @@ -113,18 +115,33 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { required: true, options: commandProps.specialPropsForInsertCommand.providedOptions, }, + localName: { + inputType: 'string', + required: true, + }, }, - onSubmit: (data) => { - commandBarActor.send({ - type: 'Find and select command', - data: { - name: 'Insert', - groupId: 'modeling', - argDefaultValues: { - path: data?.path, - }, - }, - }) + onSubmit: async (data) => { + if (!data) { + return new Error('No input provided') + } + + const ast = kclManager.ast + const { path, localName } = data + const { modifiedAst, pathToImportNode, pathToInsertNode } = + addImportAndInsert({ + node: ast, + path, + localName, + }) + await updateModelingState( + modifiedAst, + EXECUTION_TYPE_REAL, + { kclManager, editorManager, codeManager }, + { + skipUpdateAst: true, + focusPath: [pathToImportNode, pathToInsertNode], + } + ) }, }, { diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index fb74810f332..15e90f05b5e 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -46,7 +46,6 @@ import { createLiteral, createLocalName } from '@src/lang/create' import { updateModelingState } from '@src/lang/modelingWorkflows' import { addHelix, - addImportAndInsert, addOffsetPlane, addShell, addSweep, @@ -339,7 +338,6 @@ export type ModelingMachineEvent = data: ModelingCommandSchema['event.parameter.edit'] } | { type: 'Export'; data: ModelingCommandSchema['Export'] } - | { type: 'Insert'; data: ModelingCommandSchema['Insert'] } | { type: 'Boolean Subtract' data: ModelingCommandSchema['Boolean Subtract'] @@ -2678,31 +2676,6 @@ export const modelingMachine = setup({ ) } ), - insertAstMod: fromPromise( - async ({ input }: { input?: ModelingCommandSchema['Insert'] }) => { - if (!input) { - return new Error('No input provided') - } - - const ast = kclManager.ast - const { path, localName } = input - const { modifiedAst, pathToImportNode, pathToInsertNode } = - addImportAndInsert({ - node: ast, - path, - localName, - }) - await updateModelingState( - modifiedAst, - EXECUTION_TYPE_REAL, - { kclManager, editorManager, codeManager }, - { - skipUpdateAst: true, - focusPath: [pathToImportNode, pathToInsertNode], - } - ) - } - ), exportFromEngine: fromPromise( async ({}: { input?: ModelingCommandSchema['Export'] }) => { return undefined as Error | undefined @@ -2851,11 +2824,6 @@ export const modelingMachine = setup({ target: '#Modeling.parameter.editing', }, - Insert: { - target: 'Inserting', - reenter: true, - }, - Export: { target: 'Exporting', guard: 'Has exportable geometry', @@ -4267,19 +4235,6 @@ export const modelingMachine = setup({ }, }, - Inserting: { - invoke: { - src: 'insertAstMod', - id: 'insertAstMod', - input: ({ event }) => { - if (event.type !== 'Insert') return undefined - return event.data - }, - onDone: ['idle'], - onError: ['idle'], - }, - }, - Exporting: { invoke: { src: 'exportFromEngine', From eaf1f09287f7427cc09151389c271b7d675cb42b Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Fri, 4 Apr 2025 11:50:29 -0400 Subject: [PATCH 10/28] Add e2e test --- e2e/playwright/fixtures/toolbarFixture.ts | 3 + e2e/playwright/point-click-assemblies.spec.ts | 106 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 e2e/playwright/point-click-assemblies.spec.ts diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts index bc5f482749d..d13fa5adf92 100644 --- a/e2e/playwright/fixtures/toolbarFixture.ts +++ b/e2e/playwright/fixtures/toolbarFixture.ts @@ -44,6 +44,7 @@ export class ToolbarFixture { featureTreePane!: Locator gizmo!: Locator gizmoDisabled!: Locator + insertButton!: Locator constructor(page: Page) { this.page = page @@ -78,6 +79,8 @@ export class ToolbarFixture { // element or two different elements can represent these states. this.gizmo = page.getByTestId('gizmo') this.gizmoDisabled = page.getByTestId('gizmo-disabled') + + this.insertButton = page.getByTestId('insert-pane-button') } get logoLink() { diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts new file mode 100644 index 00000000000..877cd887ae4 --- /dev/null +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -0,0 +1,106 @@ +import * as fsp from 'fs/promises' +import path from 'path' + +import { executorInputPath } from '@e2e/playwright/test-utils' +import { test } from '@e2e/playwright/zoo-test' + +// test file is for testing point an click code gen functionality that's assemblies related +test.describe('Point-and-click assemblies tests', () => { + test( + `Insert kcl part into assembly as whole module import`, + { tag: ['@electron'] }, + async ({ + context, + page, + homePage, + scene, + editor, + toolbar, + cmdBar, + tronApp, + }) => { + if (!tronApp) { + fail() + } + + // One dumb hardcoded screen pixel value + const testPoint = { x: 575, y: 200 } + const initialColor: [number, number, number] = [50, 50, 50] + const partColor: [number, number, number] = [150, 150, 150] + const tolerance = 50 + + await test.step('Setup parts and expect empty assembly scene', async () => { + const projectName = 'assembly' + await context.folderSetupFn(async (dir) => { + const bracketDir = path.join(dir, projectName) + await fsp.mkdir(bracketDir, { recursive: true }) + await Promise.all([ + fsp.copyFile( + executorInputPath('cylinder-inches.kcl'), + path.join(bracketDir, 'cylinder.kcl') + ), + fsp.copyFile( + executorInputPath('e2e-can-sketch-on-chamfer.kcl'), + path.join(bracketDir, 'bracket.kcl') + ), + fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''), + ]) + }) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.openProject(projectName) + await scene.waitForExecutionDone() + await scene.expectPixelColor(initialColor, testPoint, tolerance) + }) + + await test.step('Insert first part into the assembly', async () => { + await toolbar.insertButton.click() + await cmdBar.selectOption({ name: 'cylinder.kcl' }).click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'localName', + currentArgValue: '', + headerArguments: { Path: 'cylinder.kcl', LocalName: '' }, + highlightedHeaderArg: 'localName', + commandName: 'Insert', + }) + await page.keyboard.insertText('cylinder') + await cmdBar.progressCmdBar() + await editor.expectEditor.toContain( + ` + import "cylinder.kcl" as cylinder + cylinder + `, + { shouldNormalise: true } + ) + await scene.expectPixelColor(partColor, testPoint, tolerance) + }) + + await test.step('Insert second part into the assembly', async () => { + await toolbar.insertButton.click() + await cmdBar.selectOption({ name: 'bracket.kcl' }).click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'localName', + currentArgValue: '', + headerArguments: { Path: 'bracket.kcl', LocalName: '' }, + highlightedHeaderArg: 'localName', + commandName: 'Insert', + }) + await page.keyboard.insertText('bracket') + await cmdBar.progressCmdBar() + await editor.expectEditor.toContain( + ` + import "bracket.kcl" as bracket + import "cylinder.kcl" as cylinder + cylinder + bracket + `, + { shouldNormalise: true } + ) + }) + + // TODO: remove + await page.waitForTimeout(10000) + } + ) +}) From 8052af847c120f1353adfa714e889d252bda8f61 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Fri, 4 Apr 2025 12:37:29 -0400 Subject: [PATCH 11/28] Clean up for review --- e2e/playwright/point-click-assemblies.spec.ts | 17 +++++++++---- .../ModelingSidebar/ModelingSidebar.tsx | 7 ++++-- src/components/ProjectSidebarMenu.tsx | 24 +++++++++++++++++++ src/lang/modifyAst.ts | 9 +++---- src/lib/kclCommands.ts | 8 +++++-- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index 877cd887ae4..d69612203b4 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -65,6 +65,12 @@ test.describe('Point-and-click assemblies tests', () => { }) await page.keyboard.insertText('cylinder') await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { Path: 'cylinder.kcl', LocalName: 'cylinder' }, + commandName: 'Insert', + }) + await cmdBar.progressCmdBar() await editor.expectEditor.toContain( ` import "cylinder.kcl" as cylinder @@ -88,19 +94,22 @@ test.describe('Point-and-click assemblies tests', () => { }) await page.keyboard.insertText('bracket') await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { Path: 'bracket.kcl', LocalName: 'bracket' }, + commandName: 'Insert', + }) + await cmdBar.progressCmdBar() await editor.expectEditor.toContain( ` - import "bracket.kcl" as bracket import "cylinder.kcl" as cylinder + import "bracket.kcl" as bracket cylinder bracket `, { shouldNormalise: true } ) }) - - // TODO: remove - await page.waitForTimeout(10000) } ) }) diff --git a/src/components/ModelingSidebar/ModelingSidebar.tsx b/src/components/ModelingSidebar/ModelingSidebar.tsx index 41a6032885b..245d8ab9eac 100644 --- a/src/components/ModelingSidebar/ModelingSidebar.tsx +++ b/src/components/ModelingSidebar/ModelingSidebar.tsx @@ -14,6 +14,7 @@ import type { } from '@src/components/ModelingSidebar/ModelingPanes' import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes' import Tooltip from '@src/components/Tooltip' +import { DEV } from '@src/env' import { useModelingContext } from '@src/hooks/useModelingContext' import { useKclContext } from '@src/lang/KclProvider' import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants' @@ -21,6 +22,7 @@ import { isDesktop } from '@src/lib/isDesktop' import { useSettings } from '@src/machines/appMachine' import { commandBarActor } from '@src/machines/commandBarMachine' import { onboardingPaths } from '@src/routes/Onboarding/paths' +import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils' interface ModelingSidebarProps { paneOpacity: '' | 'opacity-20' | 'opacity-40' @@ -62,10 +64,11 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { const sidebarActions: SidebarAction[] = [ { id: 'insert', - title: 'Insert from file', - sidebarName: 'Insert from file', + title: 'Insert from project file', + sidebarName: 'Insert from project file', icon: 'import', keybinding: 'Ctrl + Shift + I', + hide: (a) => a.platform === 'web' || !(DEV || IS_NIGHTLY_OR_DEBUG), action: () => commandBarActor.send({ type: 'Find and select command', diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index d8726c5a798..ea6fff60d2a 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -113,6 +113,7 @@ function ProjectMenuPopover({ const commands = useSelector(commandBarActor, commandsSelector) const { onProjectClose } = useLspContext() + const insertCommandInfo = { name: 'Insert', groupId: 'code' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' } const makeCommandInfo = { name: 'Make', groupId: 'modeling' } const shareCommandInfo = { name: 'share-file-link', groupId: 'code' } @@ -145,6 +146,29 @@ function ProjectMenuPopover({ }, }, 'break', + { + id: 'insert', + Element: 'button', + children: ( + <> + Insert from project file + {!findCommand(insertCommandInfo) && ( + + Awaiting engine connection + + )} + + ), + disabled: !findCommand(insertCommandInfo), + onClick: () => + commandBarActor.send({ + type: 'Find and select command', + data: insertCommandInfo, + }), + }, { id: 'export', Element: 'button', diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index a65c13d3f24..75733251512 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -800,13 +800,15 @@ export function addImportAndInsert({ const modifiedAst = structuredClone(node) // Add import statement - // TODO: add it to the end of existing imports, only if it doesn't exist const importStatement = createImportStatement( createImportAsSelector(localName), { type: 'Kcl', filename: path } ) - const importIndex = 0 - modifiedAst.body.unshift(importStatement) + const lastImportIndex = node.body.findLastIndex( + (v) => v.type === 'ImportStatement' + ) + const importIndex = lastImportIndex + 1 // either -1 + 1 = 0 or after the last import + modifiedAst.body.splice(importIndex, 0, importStatement) const pathToImportNode: PathToNode = [ ['body', ''], [importIndex, 'index'], @@ -814,7 +816,6 @@ export function addImportAndInsert({ ] // Add insert statement - // TODO: check that pushing to the end of the body is good here const insertStatement = createExpressionStatement(createLocalName(localName)) const insertIndex = modifiedAst.body.length modifiedAst.body.push(insertStatement) diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index d125c7bdd8f..9ec887a64d8 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -2,6 +2,7 @@ import type { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import toast from 'react-hot-toast' import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning' +import { DEV } from '@src/env' import { updateModelingState } from '@src/lang/modelingWorkflows' import { addImportAndInsert } from '@src/lang/modifyAst' import { @@ -22,6 +23,7 @@ import { baseUnitsUnion } from '@src/lib/settings/settingsTypes' import { codeManager, editorManager, kclManager } from '@src/lib/singletons' import { err, reportRejection } from '@src/lib/trap' import type { IndexLoaderData } from '@src/lib/types' +import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils' interface OnSubmitProps { sampleName: string @@ -106,9 +108,11 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { name: 'Insert', description: 'Insert from a file in the current project directory', icon: 'import', - needsReview: false, groupId: 'code', - hide: 'web', + hide: DEV || IS_NIGHTLY_OR_DEBUG ? 'web' : 'both', + needsReview: true, + reviewMessage: + 'Reminder: point-and-click insert is in development and only supports one part instance per assembly.', args: { path: { inputType: 'options', From 8fa8b164884d122670fd7d388e16af1ade2435f3 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Fri, 4 Apr 2025 12:49:52 -0400 Subject: [PATCH 12/28] Add native file menu entry and test --- e2e/playwright/native-file-menu.spec.ts | 37 +++++++++++++++++++++++++ src/components/FileMachineProvider.tsx | 5 +++- src/menu/channels.ts | 1 + src/menu/fileRole.ts | 9 ++++++ src/menu/register.ts | 8 ++++++ src/menu/roles.ts | 1 + 6 files changed, 60 insertions(+), 1 deletion(-) diff --git a/e2e/playwright/native-file-menu.spec.ts b/e2e/playwright/native-file-menu.spec.ts index bd8cf3c959a..bf5704a13bb 100644 --- a/e2e/playwright/native-file-menu.spec.ts +++ b/e2e/playwright/native-file-menu.spec.ts @@ -438,6 +438,43 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { const expected = 'Open sample' expect(actual).toBe(expected) }) + test('Modeling.File.Insert from project file', async ({ + tronApp, + cmdBar, + page, + homePage, + scene, + }) => { + if (!tronApp) { + throwTronAppMissing() + return + } + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // Run electron snippet to find the Menu! + await page.waitForTimeout(100) // wait for createModelingPageMenu() to run + await tronApp.electron.evaluate(async ({ app }) => { + if (!app || !app.applicationMenu) { + throw new Error('app or app.applicationMenu is missing') + } + const openProject = app.applicationMenu.getMenuItemById( + 'File.Insert from project file' + ) + if (!openProject) { + throw new Error('File.Insert from project file') + } + openProject.click() + }) + // Check that the command bar is opened + await expect(cmdBar.cmdBarElement).toBeVisible() + // Check the placeholder project name exists + const actual = await cmdBar.cmdBarElement + .getByTestId('command-name') + .textContent() + const expected = 'Insert' + expect(actual).toBe(expected) + }) test('Modeling.File.Export current part', async ({ tronApp, cmdBar, diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index 5c851b6085b..06a189d7561 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -467,7 +467,10 @@ export const FileMachineProvider = ({ })), }, specialPropsForInsertCommand: { - providedOptions: (project?.children ?? []).flatMap((v) => { + providedOptions: (isDesktop() && project?.children + ? project.children + : [] + ).flatMap((v) => { // TODO: add support for full tree traversal when KCL support subdir imports const relativeFilePath = v.path.replace( project?.path + window.electron.sep, diff --git a/src/menu/channels.ts b/src/menu/channels.ts index 39564033326..27dbec6e4a9 100644 --- a/src/menu/channels.ts +++ b/src/menu/channels.ts @@ -25,6 +25,7 @@ export type MenuLabels = | 'File.Create new file' | 'File.Create new folder' | 'File.Load a sample model' + | 'File.Insert from project file' | 'File.Export current part' | 'File.Share current part (via Zoo link)' | 'File.Preferences.Project settings' diff --git a/src/menu/fileRole.ts b/src/menu/fileRole.ts index df9629a3788..eedd6e866a7 100644 --- a/src/menu/fileRole.ts +++ b/src/menu/fileRole.ts @@ -157,6 +157,15 @@ export const modelingFileRole = ( }, }, { type: 'separator' }, + { + label: 'Insert from project file', + id: 'File.Insert from project file', + click: () => { + typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { + menuLabel: 'File.Insert from project file', + }) + }, + }, { label: 'Export current part', id: 'File.Export current part', diff --git a/src/menu/register.ts b/src/menu/register.ts index 8778c0db72a..fd7a5c2c7f5 100644 --- a/src/menu/register.ts +++ b/src/menu/register.ts @@ -92,6 +92,14 @@ export function modelingMenuCallbackMostActions( }).catch(reportRejection) } else if (data.menuLabel === 'File.Preferences.User default units') { navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit') + } else if (data.menuLabel === 'File.Insert from project file') { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'code', + name: 'Insert', + }, + }) } else if (data.menuLabel === 'File.Export current part') { commandBarActor.send({ type: 'Find and select command', diff --git a/src/menu/roles.ts b/src/menu/roles.ts index 80430a1da7a..513514ee1b2 100644 --- a/src/menu/roles.ts +++ b/src/menu/roles.ts @@ -21,6 +21,7 @@ type FileRoleLabel = | 'Sign out' | 'Theme' | 'Theme color' + | 'Insert from project file' | 'Export current part' | 'Create new file' | 'Create new folder' From 5f11d61321aa89ddf85e867092bb8757c4919861 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Fri, 4 Apr 2025 13:09:37 -0400 Subject: [PATCH 13/28] No await yo lint said so --- src/lib/kclCommands.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index 9ec887a64d8..f72f0f86942 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -124,7 +124,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { required: true, }, }, - onSubmit: async (data) => { + onSubmit: (data) => { if (!data) { return new Error('No input provided') } @@ -137,7 +137,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { path, localName, }) - await updateModelingState( + updateModelingState( modifiedAst, EXECUTION_TYPE_REAL, { kclManager, editorManager, codeManager }, @@ -145,7 +145,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { skipUpdateAst: true, focusPath: [pathToImportNode, pathToInsertNode], } - ) + ).catch(reportRejection) }, }, { From f36905b7a4111e41f30bd144bc5f87d1d29ef0f5 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Fri, 4 Apr 2025 16:22:58 -0400 Subject: [PATCH 14/28] WIP: UX improvements around foreign file imports Fixes #6152 --- src/components/FileTree.tsx | 33 ++++++++++++++++++-------- src/components/ToastInsert.tsx | 40 ++++++++++++++++++++++++++++++++ src/lib/constants.ts | 15 +++++++++++- src/lib/desktop.ts | 11 ++------- src/lib/getCurrentProjectFile.ts | 38 ++++++++---------------------- 5 files changed, 89 insertions(+), 48 deletions(-) create mode 100644 src/components/ToastInsert.tsx diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index c950c128af5..2949c5c054b 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -19,7 +19,7 @@ import { useKclContext } from '@src/lang/KclProvider' import type { KCLError } from '@src/lang/errors' import { kclErrorsByFilename } from '@src/lang/errors' import { normalizeLineEndings } from '@src/lib/codeEditor' -import { FILE_EXT } from '@src/lib/constants' +import { FILE_EXT, INSERT_FOREIGN_TOAST_ID } from '@src/lib/constants' import { sortFilesAndDirectories } from '@src/lib/desktopFS' import useHotkeyWrapper from '@src/lib/hotkeyWrapper' import { PATHS } from '@src/lib/paths' @@ -28,7 +28,10 @@ import { codeManager, kclManager } from '@src/lib/singletons' import { reportRejection } from '@src/lib/trap' import type { IndexLoaderData } from '@src/lib/types' +import { commandBarActor } from '@src/machines/commandBarMachine' +import toast from 'react-hot-toast' import styles from './FileTree.module.css' +import { ToastInsert } from './ToastInsert' function getIndentationCSS(level: number) { return `calc(1rem * ${level + 1})` @@ -264,16 +267,26 @@ const FileTreeItem = ({ if (fileOrDir.children !== null) return // Don't open directories if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) { - // Import non-kcl files - // We want to update both the state and editor here. - codeManager.updateCodeStateEditor( - `import("${fileOrDir.path.replace(project.path, '.')}")\n` + - codeManager.code + toast.custom( + ToastInsert({ + onInsert: () => { + const relativeFilePath = fileOrDir.path.replace( + project.path + window.electron.sep, + '' + ) + commandBarActor.send({ + type: 'Find and select command', + data: { + name: 'Insert', + groupId: 'code', + argDefaultValues: { path: relativeFilePath }, + }, + }) + toast.dismiss(INSERT_FOREIGN_TOAST_ID) + }, + }), + { duration: 30000, id: INSERT_FOREIGN_TOAST_ID } ) - await codeManager.writeToFile() - - // Prevent seeing the model built one piece at a time when changing files - await kclManager.executeCode() } else { // Let the lsp servers know we closed a file. onFileClose(currentFile?.path || null, project?.path || null) diff --git a/src/components/ToastInsert.tsx b/src/components/ToastInsert.tsx new file mode 100644 index 00000000000..e80abf6846c --- /dev/null +++ b/src/components/ToastInsert.tsx @@ -0,0 +1,40 @@ +import toast from 'react-hot-toast' + +import { ActionButton } from '@src/components/ActionButton' + +export function ToastInsert({ onInsert }: { onInsert: () => void }) { + return ( +
+
+

+ Non-KCL files aren't editable here in Zoo Studio, but you may insert + them using the button below or the Insert command. +

+
+ + Insert into my current file + + { + toast.dismiss() + }} + > + Dismiss + +
+
+
+ ) +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 96e8839fb2d..c59bd4b2d06 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,4 +1,5 @@ import type { Models } from '@kittycad/lib/dist/types/src' +import { FileImportFormat_type } from '@kittycad/lib/dist/types/src/models' import type { UnitAngle, UnitLength } from '@rust/kcl-lib/bindings/ModelingCmd' @@ -37,13 +38,22 @@ export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const export const DEFAULT_FILE_NAME = 'Untitled' /** The file endings that will appear in * the file explorer if found in a project directory */ -export const RELEVANT_FILE_TYPES = [ +// TODO: make stp part of this enum as an alias to step +// TODO: make glb part of this enum as it is in fact supported +export const RELEVANT_FILE_TYPES: ( + | FileImportFormat_type + | 'stp' + | 'glb' + | 'kcl' +)[] = [ 'kcl', 'fbx', 'gltf', 'glb', 'obj', 'ply', + 'sldprt', + 'stp', 'step', 'stl', ] as const @@ -134,6 +144,9 @@ export const CREATE_FILE_URL_PARAM = 'create-file' /** Toast id for the app auto-updater toast */ export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast' +/** Toast id for the insert foreign part toast */ +export const INSERT_FOREIGN_TOAST_ID = 'insert-foreign-toast' + /** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */ export const KCL_AXIS_X = 'X' export const KCL_AXIS_Y = 'Y' diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index bb69f170df8..c2d1cf7a117 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -16,6 +16,7 @@ import { PROJECT_FOLDER, PROJECT_IMAGE_NAME, PROJECT_SETTINGS_FILE_NAME, + RELEVANT_FILE_TYPES, SETTINGS_FILE_NAME, TELEMETRY_FILE_NAME, TELEMETRY_RAW_FILE_NAME, @@ -199,16 +200,8 @@ export async function listProjects( return projects } -const IMPORT_FILE_EXTENSIONS = [ - // TODO Use ImportFormat enum - 'stp', - 'glb', - 'fbxb', - 'kcl', -] - const isRelevantFile = (filename: string): boolean => - IMPORT_FILE_EXTENSIONS.some((ext) => filename.endsWith('.' + ext)) + RELEVANT_FILE_TYPES.some((ext) => filename.endsWith('.' + ext)) const collectAllFilesRecursiveFrom = async ( path: string, diff --git a/src/lib/getCurrentProjectFile.ts b/src/lib/getCurrentProjectFile.ts index 10235d802cb..07a6b5b5f37 100644 --- a/src/lib/getCurrentProjectFile.ts +++ b/src/lib/getCurrentProjectFile.ts @@ -1,31 +1,12 @@ -import type { Models } from '@kittycad/lib/dist/types/src' import type { Stats } from 'fs' import * as fs from 'fs/promises' import * as path from 'path' -import { PROJECT_ENTRYPOINT } from '@src/lib/constants' - -// Create a const object with the values -const FILE_IMPORT_FORMATS = { - fbx: 'fbx', - gltf: 'gltf', - obj: 'obj', - ply: 'ply', - sldprt: 'sldprt', - step: 'step', - stl: 'stl', -} as const - -// Extract the values into an array -const fileImportFormats: Models['FileImportFormat_type'][] = - Object.values(FILE_IMPORT_FORMATS) -export const allFileImportFormats: string[] = [ - ...fileImportFormats, - 'stp', - 'fbxb', - 'glb', -] -export const relevantExtensions = ['kcl', ...allFileImportFormats] +import { PROJECT_ENTRYPOINT, RELEVANT_FILE_TYPES } from '@src/lib/constants' + +const relevantExtensions = Object.fromEntries( + RELEVANT_FILE_TYPES.map((t) => [t, t]) +) /// Get the current project file from the path. /// This is used for double-clicking on a file in the file explorer, @@ -85,9 +66,9 @@ export default async function getCurrentProjectFile( // Check if the extension on what we are trying to open is a relevant file type. const extension = path.extname(sourcePath).slice(1) - if (!relevantExtensions.includes(extension) && extension !== 'toml') { + if (!relevantExtensions[extension] && extension !== 'toml') { return new Error( - `File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${relevantExtensions.join( + `File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${RELEVANT_FILE_TYPES.join( ', ' )}` ) @@ -99,7 +80,7 @@ export default async function getCurrentProjectFile( // If we got an import model file, we need to check if we have a file in the project for // this import model. - if (allFileImportFormats.includes(extension)) { + if (relevantExtensions[extension]) { const importFileName = path.basename(sourcePath) // Check if we have a file in the project for this import model. const kclWrapperFilename = `${importFileName}.kcl` @@ -115,7 +96,8 @@ export default async function getCurrentProjectFile( // But we recommend you keep the import statement as it is. // For more information on the import statement, see the documentation at: // https://zoo.dev/docs/kcl/import -const model = import("${importFileName}")` +import "${importFileName}" as model +model` await fs.writeFile(kclWrapperFilePath, content) } From 4698339301087aed4f4b3c7e476890b9c05d4e0e Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Mon, 7 Apr 2025 08:37:11 -0400 Subject: [PATCH 15/28] @lrev-Dev's suggestion to remove a comment Co-authored-by: Kurt Hutten --- src/lang/modelingWorkflows.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lang/modelingWorkflows.ts b/src/lang/modelingWorkflows.ts index 556d512fb26..6c34bed2be7 100644 --- a/src/lang/modelingWorkflows.ts +++ b/src/lang/modelingWorkflows.ts @@ -66,7 +66,6 @@ export async function updateModelingState( // Step 1: Update AST without executing (prepare selections) updatedAst = await dependencies.kclManager.updateAst( ast, - // false == mock execution. Is this what we want? false, // Execution handled separately for error resilience options ) From c5e18bab17d61d411c0e14ab61178ff0ba81d1f8 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Mon, 7 Apr 2025 09:25:01 -0400 Subject: [PATCH 16/28] Update to scene.settled(cmdBar) --- e2e/playwright/point-click-assemblies.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index d69612203b4..fab574404e7 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -48,7 +48,7 @@ test.describe('Point-and-click assemblies tests', () => { }) await page.setBodyDimensions({ width: 1000, height: 500 }) await homePage.openProject(projectName) - await scene.waitForExecutionDone() + await scene.settled(cmdBar) await scene.expectPixelColor(initialColor, testPoint, tolerance) }) From 7689afbbf1f6484f5f32a64d9fd1c25ec5fd31c3 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Mon, 7 Apr 2025 14:38:05 -0400 Subject: [PATCH 17/28] Add partNNN default name for alias --- e2e/playwright/point-click-assemblies.spec.ts | 16 +++++++++------- src/components/FileMachineProvider.tsx | 14 ++++++++++++-- src/lib/kclCommands.ts | 2 ++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index fab574404e7..5ce8022ae88 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -2,7 +2,7 @@ import * as fsp from 'fs/promises' import path from 'path' import { executorInputPath } from '@e2e/playwright/test-utils' -import { test } from '@e2e/playwright/zoo-test' +import { expect, test } from '@e2e/playwright/zoo-test' // test file is for testing point an click code gen functionality that's assemblies related test.describe('Point-and-click assemblies tests', () => { @@ -63,21 +63,22 @@ test.describe('Point-and-click assemblies tests', () => { highlightedHeaderArg: 'localName', commandName: 'Insert', }) - await page.keyboard.insertText('cylinder') + await expect(cmdBar.argumentInput).toHaveValue('part001') await cmdBar.progressCmdBar() await cmdBar.expectState({ stage: 'review', - headerArguments: { Path: 'cylinder.kcl', LocalName: 'cylinder' }, + headerArguments: { Path: 'cylinder.kcl', LocalName: 'part001' }, commandName: 'Insert', }) await cmdBar.progressCmdBar() await editor.expectEditor.toContain( ` - import "cylinder.kcl" as cylinder - cylinder + import "cylinder.kcl" as part001 + part001 `, { shouldNormalise: true } ) + await scene.settled(cmdBar) await scene.expectPixelColor(partColor, testPoint, tolerance) }) @@ -92,6 +93,7 @@ test.describe('Point-and-click assemblies tests', () => { highlightedHeaderArg: 'localName', commandName: 'Insert', }) + await expect(cmdBar.argumentInput).toHaveValue('part002') await page.keyboard.insertText('bracket') await cmdBar.progressCmdBar() await cmdBar.expectState({ @@ -102,9 +104,9 @@ test.describe('Point-and-click assemblies tests', () => { await cmdBar.progressCmdBar() await editor.expectEditor.toContain( ` - import "cylinder.kcl" as cylinder + import "cylinder.kcl" as part001 import "bracket.kcl" as bracket - cylinder + part001 bracket `, { shouldNormalise: true } diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index ac5279d689f..386dbd3c758 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -13,6 +13,7 @@ import { fromPromise } from 'xstate' import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath' import { useMenuListener } from '@src/hooks/useMenu' +import { findUniqueName } from '@src/lang/create' import { newKclFile } from '@src/lang/project' import { createNamedViewsCommand } from '@src/lib/commandBarConfigs/namedViewsConfig' import { createRouteCommands } from '@src/lib/commandBarConfigs/routeCommandConfig' @@ -460,6 +461,7 @@ export const FileMachineProvider = ({ })), }, specialPropsForInsertCommand: { + defaultName: findUniqueName(kclManager.ast, 'part'), providedOptions: (isDesktop() && project?.children ? project.children : [] @@ -469,7 +471,7 @@ export const FileMachineProvider = ({ project?.path + window.electron.sep, '' ) - const isDirectory = v.children + const isDirectory = !!v.children const isCurrentFile = v.path === file?.path return isDirectory || isCurrentFile ? [] @@ -482,7 +484,15 @@ export const FileMachineProvider = ({ }).filter( (command) => kclSamples.length || command.name !== 'open-kcl-example' ), - [codeManager, kclManager, send, kclSamples, project, file] + [ + codeManager, + kclManager, + send, + kclSamples, + project, + file, + kclManager.ast.body, + ] ) useEffect(() => { diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index f72f0f86942..6a3cbad0b14 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -40,6 +40,7 @@ interface KclCommandConfig { providedOptions: CommandArgumentOption[] } specialPropsForInsertCommand: { + defaultName: string providedOptions: CommandArgumentOption[] } projectData: IndexLoaderData @@ -122,6 +123,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { localName: { inputType: 'string', required: true, + defaultValue: commandProps.specialPropsForInsertCommand.defaultName, }, }, onSubmit: (data) => { From cbaf345c52a61444d86bd4492ff4fb2e0402f0b1 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Mon, 7 Apr 2025 17:39:46 -0400 Subject: [PATCH 18/28] Lint --- src/components/FileMachineProvider.tsx | 1 - src/components/FileTree.tsx | 2 +- src/components/ToastInsert.tsx | 2 +- src/lib/constants.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index ae75fbe9f9f..ac5279d689f 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -13,7 +13,6 @@ import { fromPromise } from 'xstate' import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath' import { useMenuListener } from '@src/hooks/useMenu' -import { findUniqueName } from '@src/lang/create' import { newKclFile } from '@src/lang/project' import { createNamedViewsCommand } from '@src/lib/commandBarConfigs/namedViewsConfig' import { createRouteCommands } from '@src/lib/commandBarConfigs/routeCommandConfig' diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index 2949c5c054b..7a9fd6a2d86 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -31,7 +31,7 @@ import type { IndexLoaderData } from '@src/lib/types' import { commandBarActor } from '@src/machines/commandBarMachine' import toast from 'react-hot-toast' import styles from './FileTree.module.css' -import { ToastInsert } from './ToastInsert' +import { ToastInsert } from '@src/components/ToastInsert' function getIndentationCSS(level: number) { return `calc(1rem * ${level + 1})` diff --git a/src/components/ToastInsert.tsx b/src/components/ToastInsert.tsx index e80abf6846c..ef4f8a77812 100644 --- a/src/components/ToastInsert.tsx +++ b/src/components/ToastInsert.tsx @@ -16,7 +16,7 @@ export function ToastInsert({ onInsert }: { onInsert: () => void }) { iconStart={{ icon: 'checkmark', }} - name="instert" + name="insert" onClick={onInsert} > Insert into my current file diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c80ceea7acb..cda9bc01578 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,5 +1,5 @@ import type { Models } from '@kittycad/lib/dist/types/src' -import { FileImportFormat_type } from '@kittycad/lib/dist/types/src/models' +import type { FileImportFormat_type } from '@kittycad/lib/dist/types/src/models' import type { UnitAngle, UnitLength } from '@rust/kcl-lib/bindings/ModelingCmd' From b098d8c7c0db70b733e933551b4f9482eca96413 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Mon, 7 Apr 2025 17:50:11 -0400 Subject: [PATCH 19/28] Lint --- src/components/FileTree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index 7a9fd6a2d86..da5bc0ec8cc 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -28,10 +28,10 @@ import { codeManager, kclManager } from '@src/lib/singletons' import { reportRejection } from '@src/lib/trap' import type { IndexLoaderData } from '@src/lib/types' +import { ToastInsert } from '@src/components/ToastInsert' import { commandBarActor } from '@src/machines/commandBarMachine' import toast from 'react-hot-toast' import styles from './FileTree.module.css' -import { ToastInsert } from '@src/components/ToastInsert' function getIndentationCSS(level: number) { return `calc(1rem * ${level + 1})` From 60c358057f7bc5631c5082cfc99a3ee4ab5c80f1 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Mon, 7 Apr 2025 19:27:05 -0400 Subject: [PATCH 20/28] Fix unit tests --- src/lib/constants.ts | 8 +++++--- src/lib/getCurrentProjectFile.ts | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index cda9bc01578..c63e4f4b60d 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -40,12 +40,14 @@ export const DEFAULT_FILE_NAME = 'Untitled' * the file explorer if found in a project directory */ // TODO: make stp part of this enum as an alias to step // TODO: make glb part of this enum as it is in fact supported -export const RELEVANT_FILE_TYPES: ( +export type NativeFileType = 'kcl' +export type RelevantFileType = | FileImportFormat_type + | NativeFileType | 'stp' | 'glb' - | 'kcl' -)[] = [ +export const NATIVE_FILE_TYPE: NativeFileType = 'kcl' +export const RELEVANT_FILE_TYPES: RelevantFileType[] = [ 'kcl', 'fbx', 'gltf', diff --git a/src/lib/getCurrentProjectFile.ts b/src/lib/getCurrentProjectFile.ts index 07a6b5b5f37..9ce3b821655 100644 --- a/src/lib/getCurrentProjectFile.ts +++ b/src/lib/getCurrentProjectFile.ts @@ -2,11 +2,16 @@ import type { Stats } from 'fs' import * as fs from 'fs/promises' import * as path from 'path' -import { PROJECT_ENTRYPOINT, RELEVANT_FILE_TYPES } from '@src/lib/constants' +import { + NATIVE_FILE_TYPE, + PROJECT_ENTRYPOINT, + RELEVANT_FILE_TYPES, + type RelevantFileType, +} from '@src/lib/constants' -const relevantExtensions = Object.fromEntries( - RELEVANT_FILE_TYPES.map((t) => [t, t]) -) +const shouldWrapExtension = (extension: string) => + RELEVANT_FILE_TYPES.includes(extension as RelevantFileType) && + extension !== NATIVE_FILE_TYPE /// Get the current project file from the path. /// This is used for double-clicking on a file in the file explorer, @@ -66,7 +71,10 @@ export default async function getCurrentProjectFile( // Check if the extension on what we are trying to open is a relevant file type. const extension = path.extname(sourcePath).slice(1) - if (!relevantExtensions[extension] && extension !== 'toml') { + if ( + !RELEVANT_FILE_TYPES.includes(extension as RelevantFileType) && + extension !== 'toml' + ) { return new Error( `File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${RELEVANT_FILE_TYPES.join( ', ' @@ -80,7 +88,9 @@ export default async function getCurrentProjectFile( // If we got an import model file, we need to check if we have a file in the project for // this import model. - if (relevantExtensions[extension]) { + // TODO: once we have some sort of a load file into project it would make sense to stop creating these wrapper files + // and let people save their own kcl file importing + if (shouldWrapExtension(extension)) { const importFileName = path.basename(sourcePath) // Check if we have a file in the project for this import model. const kclWrapperFilename = `${importFileName}.kcl` From 3c388806436344dbf2ab59c99ac90249cc2ea0ea Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Mon, 7 Apr 2025 19:41:12 -0400 Subject: [PATCH 21/28] Add sad path insert test Thanks @Irev-Dev for the suggestion --- e2e/playwright/point-click-assemblies.spec.ts | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index fab574404e7..1a158ec77c4 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -2,7 +2,7 @@ import * as fsp from 'fs/promises' import path from 'path' import { executorInputPath } from '@e2e/playwright/test-utils' -import { test } from '@e2e/playwright/zoo-test' +import { expect, test } from '@e2e/playwright/zoo-test' // test file is for testing point an click code gen functionality that's assemblies related test.describe('Point-and-click assemblies tests', () => { @@ -25,7 +25,6 @@ test.describe('Point-and-click assemblies tests', () => { // One dumb hardcoded screen pixel value const testPoint = { x: 575, y: 200 } - const initialColor: [number, number, number] = [50, 50, 50] const partColor: [number, number, number] = [150, 150, 150] const tolerance = 50 @@ -49,28 +48,31 @@ test.describe('Point-and-click assemblies tests', () => { await page.setBodyDimensions({ width: 1000, height: 500 }) await homePage.openProject(projectName) await scene.settled(cmdBar) - await scene.expectPixelColor(initialColor, testPoint, tolerance) }) - await test.step('Insert first part into the assembly', async () => { + async function insertPartIntoAssembly(path: string, alias: string) { await toolbar.insertButton.click() - await cmdBar.selectOption({ name: 'cylinder.kcl' }).click() + await cmdBar.selectOption({ name: path }).click() await cmdBar.expectState({ stage: 'arguments', currentArgKey: 'localName', currentArgValue: '', - headerArguments: { Path: 'cylinder.kcl', LocalName: '' }, + headerArguments: { Path: path, LocalName: '' }, highlightedHeaderArg: 'localName', commandName: 'Insert', }) - await page.keyboard.insertText('cylinder') + await page.keyboard.insertText(alias) await cmdBar.progressCmdBar() await cmdBar.expectState({ stage: 'review', - headerArguments: { Path: 'cylinder.kcl', LocalName: 'cylinder' }, + headerArguments: { Path: path, LocalName: alias }, commandName: 'Insert', }) await cmdBar.progressCmdBar() + } + + await test.step('Insert first part into the assembly', async () => { + await insertPartIntoAssembly('cylinder.kcl', 'cylinder') await editor.expectEditor.toContain( ` import "cylinder.kcl" as cylinder @@ -78,28 +80,12 @@ test.describe('Point-and-click assemblies tests', () => { `, { shouldNormalise: true } ) + await scene.settled(cmdBar) await scene.expectPixelColor(partColor, testPoint, tolerance) }) await test.step('Insert second part into the assembly', async () => { - await toolbar.insertButton.click() - await cmdBar.selectOption({ name: 'bracket.kcl' }).click() - await cmdBar.expectState({ - stage: 'arguments', - currentArgKey: 'localName', - currentArgValue: '', - headerArguments: { Path: 'bracket.kcl', LocalName: '' }, - highlightedHeaderArg: 'localName', - commandName: 'Insert', - }) - await page.keyboard.insertText('bracket') - await cmdBar.progressCmdBar() - await cmdBar.expectState({ - stage: 'review', - headerArguments: { Path: 'bracket.kcl', LocalName: 'bracket' }, - commandName: 'Insert', - }) - await cmdBar.progressCmdBar() + await insertPartIntoAssembly('bracket.kcl', 'bracket') await editor.expectEditor.toContain( ` import "cylinder.kcl" as cylinder @@ -109,6 +95,25 @@ test.describe('Point-and-click assemblies tests', () => { `, { shouldNormalise: true } ) + await scene.settled(cmdBar) + }) + + await test.step('Insert a second time and expect error', async () => { + // TODO: revisit once we have clone with #6209 + await insertPartIntoAssembly('bracket.kcl', 'bracket') + await editor.expectEditor.toContain( + ` + import "cylinder.kcl" as cylinder + import "bracket.kcl" as bracket + import "bracket.kcl" as bracket + cylinder + bracket + bracket + `, + { shouldNormalise: true } + ) + await scene.settled(cmdBar) + await expect(page.locator('.cm-lint-marker-error')).toBeVisible() }) } ) From 757466310d4ee63ca0e3de39986d655a61a81baf Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Tue, 8 Apr 2025 07:08:59 -0400 Subject: [PATCH 22/28] Add step insert test --- e2e/playwright/point-click-assemblies.spec.ts | 183 ++++++++++++++---- e2e/playwright/test-utils.ts | 4 + 2 files changed, 154 insertions(+), 33 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index 1a158ec77c4..956b69f2705 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -1,13 +1,47 @@ import * as fsp from 'fs/promises' import path from 'path' -import { executorInputPath } from '@e2e/playwright/test-utils' +import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture' +import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' +import { + executorInputPath, + getUtils, + testsInputPath, +} from '@e2e/playwright/test-utils' import { expect, test } from '@e2e/playwright/zoo-test' +import { Page } from '@playwright/test' + +async function insertPartIntoAssembly( + path: string, + alias: string, + toolbar: ToolbarFixture, + cmdBar: CmdBarFixture, + page: Page +) { + await toolbar.insertButton.click() + await cmdBar.selectOption({ name: path }).click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'localName', + currentArgValue: '', + headerArguments: { Path: path, LocalName: '' }, + highlightedHeaderArg: 'localName', + commandName: 'Insert', + }) + await page.keyboard.insertText(alias) + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { Path: path, LocalName: alias }, + commandName: 'Insert', + }) + await cmdBar.progressCmdBar() +} // test file is for testing point an click code gen functionality that's assemblies related test.describe('Point-and-click assemblies tests', () => { test( - `Insert kcl part into assembly as whole module import`, + `Insert kcl parts into assembly as whole module import`, { tag: ['@electron'] }, async ({ context, @@ -23,10 +57,14 @@ test.describe('Point-and-click assemblies tests', () => { fail() } - // One dumb hardcoded screen pixel value - const testPoint = { x: 575, y: 200 } - const partColor: [number, number, number] = [150, 150, 150] + const midPoint = { x: 500, y: 250 } + const partPoint = { x: midPoint.x + 30, y: midPoint.y - 30 } // mid point, just off top right + const defaultPlanesColor: [number, number, number] = [180, 220, 180] + const partColor: [number, number, number] = [100, 100, 100] const tolerance = 50 + const u = await getUtils(page) + const gizmo = page.locator('[aria-label*=gizmo]') + const resetCameraButton = page.getByRole('button', { name: 'Reset view' }) await test.step('Setup parts and expect empty assembly scene', async () => { const projectName = 'assembly' @@ -35,44 +73,36 @@ test.describe('Point-and-click assemblies tests', () => { await fsp.mkdir(bracketDir, { recursive: true }) await Promise.all([ fsp.copyFile( - executorInputPath('cylinder-inches.kcl'), + executorInputPath('cylinder.kcl'), path.join(bracketDir, 'cylinder.kcl') ), fsp.copyFile( executorInputPath('e2e-can-sketch-on-chamfer.kcl'), path.join(bracketDir, 'bracket.kcl') ), + fsp.copyFile( + testsInputPath('cube.step'), + path.join(bracketDir, 'cube.step') + ), fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''), ]) }) await page.setBodyDimensions({ width: 1000, height: 500 }) await homePage.openProject(projectName) await scene.settled(cmdBar) + await toolbar.closePane('code') + await scene.expectPixelColor(defaultPlanesColor, midPoint, tolerance) }) - async function insertPartIntoAssembly(path: string, alias: string) { - await toolbar.insertButton.click() - await cmdBar.selectOption({ name: path }).click() - await cmdBar.expectState({ - stage: 'arguments', - currentArgKey: 'localName', - currentArgValue: '', - headerArguments: { Path: path, LocalName: '' }, - highlightedHeaderArg: 'localName', - commandName: 'Insert', - }) - await page.keyboard.insertText(alias) - await cmdBar.progressCmdBar() - await cmdBar.expectState({ - stage: 'review', - headerArguments: { Path: path, LocalName: alias }, - commandName: 'Insert', - }) - await cmdBar.progressCmdBar() - } - - await test.step('Insert first part into the assembly', async () => { - await insertPartIntoAssembly('cylinder.kcl', 'cylinder') + await test.step('Insert kcl as first part as module', async () => { + await insertPartIntoAssembly( + 'cylinder.kcl', + 'cylinder', + toolbar, + cmdBar, + page + ) + await toolbar.openPane('code') await editor.expectEditor.toContain( ` import "cylinder.kcl" as cylinder @@ -81,11 +111,26 @@ test.describe('Point-and-click assemblies tests', () => { { shouldNormalise: true } ) await scene.settled(cmdBar) - await scene.expectPixelColor(partColor, testPoint, tolerance) + + // Check scene for changes + await toolbar.closePane('code') + await u.doAndWaitForCmd(async () => { + await gizmo.click({ button: 'right' }) + await resetCameraButton.click() + }, 'zoom_to_fit') + await toolbar.closePane('debug') + await scene.expectPixelColor(partColor, partPoint, tolerance) + await toolbar.openPane('code') }) - await test.step('Insert second part into the assembly', async () => { - await insertPartIntoAssembly('bracket.kcl', 'bracket') + await test.step('Insert kcl second part as module', async () => { + await insertPartIntoAssembly( + 'bracket.kcl', + 'bracket', + toolbar, + cmdBar, + page + ) await editor.expectEditor.toContain( ` import "cylinder.kcl" as cylinder @@ -100,7 +145,13 @@ test.describe('Point-and-click assemblies tests', () => { await test.step('Insert a second time and expect error', async () => { // TODO: revisit once we have clone with #6209 - await insertPartIntoAssembly('bracket.kcl', 'bracket') + await insertPartIntoAssembly( + 'bracket.kcl', + 'bracket', + toolbar, + cmdBar, + page + ) await editor.expectEditor.toContain( ` import "cylinder.kcl" as cylinder @@ -117,4 +168,70 @@ test.describe('Point-and-click assemblies tests', () => { }) } ) + + test( + `Insert foreign parts into assembly as whole module import`, + { tag: ['@electron'] }, + async ({ + context, + page, + homePage, + scene, + editor, + toolbar, + cmdBar, + tronApp, + }) => { + if (!tronApp) { + fail() + } + + const midPoint = { x: 500, y: 250 } + const partPoint = { x: midPoint.x + 30, y: midPoint.y - 30 } // mid point, just off top right + const defaultPlanesColor: [number, number, number] = [180, 220, 180] + const partColor: [number, number, number] = [150, 150, 150] + const tolerance = 50 + + await test.step('Setup parts and expect empty assembly scene', async () => { + const projectName = 'assembly' + await context.folderSetupFn(async (dir) => { + const bracketDir = path.join(dir, projectName) + await fsp.mkdir(bracketDir, { recursive: true }) + await Promise.all([ + fsp.copyFile( + testsInputPath('cube.step'), + path.join(bracketDir, 'cube.step') + ), + fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''), + ]) + }) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.openProject(projectName) + await scene.settled(cmdBar) + await toolbar.closePane('code') + await scene.expectPixelColor(defaultPlanesColor, midPoint, tolerance) + }) + + await test.step('Insert step part as module', async () => { + await insertPartIntoAssembly('cube.step', 'cube', toolbar, cmdBar, page) + await toolbar.openPane('code') + await editor.expectEditor.toContain( + ` + import "cube.step" as cube + cube + `, + { shouldNormalise: true } + ) + await scene.settled(cmdBar) + + // TODO: remove this once #5780 is fixed + await page.reload() + + await scene.settled(cmdBar) + await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() + await toolbar.closePane('code') + await scene.expectPixelColor(partColor, partPoint, tolerance) + }) + } + ) }) diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index 3396e38b108..524dda15dfa 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -1021,6 +1021,10 @@ export function executorInputPath(fileName: string): string { return path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName) } +export function testsInputPath(fileName: string): string { + return path.join('rust', 'kcl-lib', 'tests', 'inputs', fileName) +} + export async function doAndWaitForImageDiff( page: Page, fn: () => Promise, From 17be7a3235fd342abdaf19d8fb929c916f500fc2 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Tue, 8 Apr 2025 07:31:26 -0400 Subject: [PATCH 23/28] Lint --- e2e/playwright/point-click-assemblies.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index 956b69f2705..3b3f3cd31db 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -1,15 +1,15 @@ import * as fsp from 'fs/promises' import path from 'path' -import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture' -import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' +import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture' +import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' import { executorInputPath, getUtils, testsInputPath, } from '@e2e/playwright/test-utils' import { expect, test } from '@e2e/playwright/zoo-test' -import { Page } from '@playwright/test' +import type { Page } from '@playwright/test' async function insertPartIntoAssembly( path: string, From ae72c7c87ffeb42e78f330626029bb1acb6a2bc3 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Tue, 8 Apr 2025 09:15:47 -0400 Subject: [PATCH 24/28] Add test for second foreign import thru file tree click --- e2e/playwright/point-click-assemblies.spec.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index 3b3f3cd31db..9067cc02435 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -202,6 +202,10 @@ test.describe('Point-and-click assemblies tests', () => { testsInputPath('cube.step'), path.join(bracketDir, 'cube.step') ), + fsp.copyFile( + testsInputPath('cube.sldprt'), + path.join(bracketDir, 'cube.sldprt') + ), fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''), ]) }) @@ -232,6 +236,45 @@ test.describe('Point-and-click assemblies tests', () => { await toolbar.closePane('code') await scene.expectPixelColor(partColor, partPoint, tolerance) }) + + await test.step('Insert second step part by clicking', async () => { + await toolbar.openPane('files') + await toolbar.expectFileTreeState([ + 'cube.sldprt', + 'cube.step', + 'main.kcl', + ]) + await toolbar.openFile('cube.sldprt') + await page.getByText('Insert into my current file').click() + await page.keyboard.insertText('cubeSw') + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { Path: 'cube.sldprt', LocalName: 'cubeSw' }, + commandName: 'Insert', + }) + await cmdBar.progressCmdBar() + await toolbar.closePane('files') + await toolbar.openPane('code') + await editor.expectEditor.toContain( + ` + import "cube.step" as cube + import "cube.sldprt" as cubeSw + cube + cubeSw + `, + { shouldNormalise: true } + ) + await scene.settled(cmdBar) + + // TODO: remove this once #5780 is fixed + await page.reload() + await scene.settled(cmdBar) + + await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() + await toolbar.closePane('code') + await scene.expectPixelColor(partColor, partPoint, tolerance) + }) } ) }) From f21c6feef4363323ba3beefb3fd88378a07fdf91 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Tue, 8 Apr 2025 15:16:02 -0400 Subject: [PATCH 25/28] Add default value for local name alias --- e2e/playwright/point-click-assemblies.spec.ts | 22 ++++++++--- .../CommandBar/CommandBarBasicInput.tsx | 27 +++++++++++--- src/lib/desktop.test.ts | 37 ++++++++++++++++++- src/lib/desktop.ts | 14 ++++++- src/lib/getCurrentProjectFile.ts | 2 +- src/lib/kclCommands.ts | 10 +++++ src/lib/utils.test.ts | 16 ++++++++ src/lib/utils.ts | 18 +++++++++ 8 files changed, 132 insertions(+), 14 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index 9067cc02435..4dd906bd7b0 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -192,6 +192,9 @@ test.describe('Point-and-click assemblies tests', () => { const partColor: [number, number, number] = [150, 150, 150] const tolerance = 50 + const complexPlmFileName = 'cube_Complex-PLM_Name_-001.SLDPRT' + const camelCasedSolidworksFileName = 'cubeComplexPLMName001' + await test.step('Setup parts and expect empty assembly scene', async () => { const projectName = 'assembly' await context.folderSetupFn(async (dir) => { @@ -204,7 +207,7 @@ test.describe('Point-and-click assemblies tests', () => { ), fsp.copyFile( testsInputPath('cube.sldprt'), - path.join(bracketDir, 'cube.sldprt') + path.join(bracketDir, complexPlmFileName) ), fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''), ]) @@ -240,17 +243,26 @@ test.describe('Point-and-click assemblies tests', () => { await test.step('Insert second step part by clicking', async () => { await toolbar.openPane('files') await toolbar.expectFileTreeState([ - 'cube.sldprt', + complexPlmFileName, 'cube.step', 'main.kcl', ]) - await toolbar.openFile('cube.sldprt') + await toolbar.openFile(complexPlmFileName) + + // Go through the ToastInsert prompt await page.getByText('Insert into my current file').click() + + // Check getCamelCaseFromFileName output + const parsedValueFromFile = + await cmdBar.currentArgumentInput.inputValue() + expect(parsedValueFromFile).toEqual(camelCasedSolidworksFileName) + + // Continue on with the flow await page.keyboard.insertText('cubeSw') await cmdBar.progressCmdBar() await cmdBar.expectState({ stage: 'review', - headerArguments: { Path: 'cube.sldprt', LocalName: 'cubeSw' }, + headerArguments: { Path: complexPlmFileName, LocalName: 'cubeSw' }, commandName: 'Insert', }) await cmdBar.progressCmdBar() @@ -259,7 +271,7 @@ test.describe('Point-and-click assemblies tests', () => { await editor.expectEditor.toContain( ` import "cube.step" as cube - import "cube.sldprt" as cubeSw + import "${complexPlmFileName}" as cubeSw cube cubeSw `, diff --git a/src/components/CommandBar/CommandBarBasicInput.tsx b/src/components/CommandBar/CommandBarBasicInput.tsx index b079af0d240..137653bc1ea 100644 --- a/src/components/CommandBar/CommandBarBasicInput.tsx +++ b/src/components/CommandBar/CommandBarBasicInput.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef } from 'react' +import { useSelector } from '@xstate/react' +import { useEffect, useMemo, useRef } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import type { CommandArgument } from '@src/lib/commandTypes' @@ -6,6 +7,11 @@ import { commandBarActor, useCommandBarState, } from '@src/machines/commandBarMachine' +import type { AnyStateMachine, SnapshotFrom } from 'xstate' + +// TODO: remove the need for this selector once we decouple all actors from React +const machineContextSelector = (snapshot?: SnapshotFrom) => + snapshot?.context function CommandBarBasicInput({ arg, @@ -22,6 +28,19 @@ function CommandBarBasicInput({ const commandBarState = useCommandBarState() useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' })) const inputRef = useRef(null) + const argMachineContext = useSelector( + arg.machineActor, + machineContextSelector + ) + const defaultValue = useMemo( + () => + arg.defaultValue + ? arg.defaultValue instanceof Function + ? arg.defaultValue(commandBarState.context, argMachineContext) + : arg.defaultValue + : '', + [arg.defaultValue, commandBarState.context, argMachineContext] + ) useEffect(() => { if (inputRef.current) { @@ -53,11 +72,7 @@ function CommandBarBasicInput({ required className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none" placeholder="Enter a value" - defaultValue={ - (commandBarState.context.argumentsToSubmit[arg.name] as - | string - | undefined) || (arg.defaultValue as string) - } + defaultValue={defaultValue} onKeyDown={(event) => { if (event.key === 'Backspace' && event.shiftKey) { stepBack() diff --git a/src/lib/desktop.test.ts b/src/lib/desktop.test.ts index bcb415f9a06..cbe1d9644b2 100644 --- a/src/lib/desktop.test.ts +++ b/src/lib/desktop.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Configuration } from '@rust/kcl-lib/bindings/Configuration' -import { listProjects } from '@src/lib/desktop' +import { isRelevantFile, listProjects } from '@src/lib/desktop' import type { DeepPartial } from '@src/lib/types' // Mock the electron window global @@ -112,6 +112,41 @@ describe('desktop utilities', () => { mockElectron.kittycad.mockResolvedValue({}) }) + describe('isRelevantFile', () => { + it('finds supported extension files relevant', () => { + expect(isRelevantFile('part.kcl')).toEqual(true) + expect(isRelevantFile('part.fbx')).toEqual(true) + expect(isRelevantFile('part.gltf')).toEqual(true) + expect(isRelevantFile('part.glb')).toEqual(true) + expect(isRelevantFile('part.obj')).toEqual(true) + expect(isRelevantFile('part.ply')).toEqual(true) + expect(isRelevantFile('part.sldprt')).toEqual(true) + expect(isRelevantFile('part.stp')).toEqual(true) + expect(isRelevantFile('part.step')).toEqual(true) + expect(isRelevantFile('part.stl')).toEqual(true) + }) + + // TODO: we should be lowercasing the extension here to check. .sldprt or .SLDPRT should be supported + // But the api doesn't allow it today, so revisit this and the tests once this is done + // it('finds supported uppercase extension files relevant', () => { + // expect(isRelevantFile('part.KCL')).toEqual(true) + // expect(isRelevantFile('part.FBX')).toEqual(true) + // expect(isRelevantFile('part.GLTF')).toEqual(true) + // expect(isRelevantFile('part.GLB')).toEqual(true) + // expect(isRelevantFile('part.OBJ')).toEqual(true) + // expect(isRelevantFile('part.PLY')).toEqual(true) + // expect(isRelevantFile('part.SLDPRT')).toEqual(true) + // expect(isRelevantFile('part.STP')).toEqual(true) + // expect(isRelevantFile('part.STEP')).toEqual(true) + // expect(isRelevantFile('part.STL')).toEqual(true) + // }) + + it("doesn't find .docx or .SLDASM relevant", () => { + expect(isRelevantFile('paper.docx')).toEqual(false) + expect(isRelevantFile('assembly.SLDASM')).toEqual(false) + }) + }) + describe('listProjects', () => { it('does not list .git directories', async () => { const projects = await listProjects(mockConfig) diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index 0ecff34300d..61feaeb0d80 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -25,6 +25,7 @@ import { import type { FileEntry, Project } from '@src/lib/project' import { err } from '@src/lib/trap' import type { DeepPartial } from '@src/lib/types' +import { getCamelCase } from '@src/lib/utils' export async function renameProjectDirectory( projectPath: string, @@ -200,7 +201,9 @@ export async function listProjects( return projects } -const isRelevantFile = (filename: string): boolean => +// TODO: we should be lowercasing the extension here to check. .sldprt or .SLDPRT should be supported +// But the api doesn't allow it today, so revisit this and the tests once this is done +export const isRelevantFile = (filename: string): boolean => RELEVANT_FILE_TYPES.some((ext) => filename.endsWith('.' + ext)) const collectAllFilesRecursiveFrom = async ( @@ -724,3 +727,12 @@ export const writeProjectThumbnailFile = async ( } return window.electron.writeFile(filePath, asArray) } + +export function getCamelCaseFromFilePath(path: string) { + // from https://nodejs.org/en/learn/manipulating-files/nodejs-file-paths#example + const basenameNoExt = window.electron.path.basename( + path, + window.electron.path.extname(path) + ) + return getCamelCase(basenameNoExt) +} diff --git a/src/lib/getCurrentProjectFile.ts b/src/lib/getCurrentProjectFile.ts index 9ce3b821655..f85c50b7fc3 100644 --- a/src/lib/getCurrentProjectFile.ts +++ b/src/lib/getCurrentProjectFile.ts @@ -69,7 +69,7 @@ export default async function getCurrentProjectFile( } // Check if the extension on what we are trying to open is a relevant file type. - const extension = path.extname(sourcePath).slice(1) + const extension = path.extname(sourcePath).slice(1).toLowerCase() if ( !RELEVANT_FILE_TYPES.includes(extension as RelevantFileType) && diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index f72f0f86942..0233d8142da 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -17,12 +17,14 @@ import { EXECUTION_TYPE_REAL, FILE_EXT, } from '@src/lib/constants' +import { getCamelCaseFromFilePath } from '@src/lib/desktop' import { isDesktop } from '@src/lib/isDesktop' import { copyFileShareLink } from '@src/lib/links' import { baseUnitsUnion } from '@src/lib/settings/settingsTypes' import { codeManager, editorManager, kclManager } from '@src/lib/singletons' import { err, reportRejection } from '@src/lib/trap' import type { IndexLoaderData } from '@src/lib/types' +import type { CommandBarContext } from '@src/machines/commandBarMachine' import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils' interface OnSubmitProps { @@ -122,6 +124,14 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { localName: { inputType: 'string', required: true, + defaultValue: (context: CommandBarContext) => { + if (!context.argumentsToSubmit['path']) { + return + } + + const path = context.argumentsToSubmit['path'] as string + return getCamelCaseFromFilePath(path) + }, }, }, onSubmit: (data) => { diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index cd792403fd3..75669b4b9d6 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -1,6 +1,7 @@ import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange' import { topLevelRange } from '@src/lang/util' import { + getCamelCase, hasDigitsLeftOfDecimal, hasLeadingZero, isClockwise, @@ -1308,3 +1309,18 @@ describe('testing isClockwise', () => { expect(isClockwise(counterClockwiseTriangle)).toBe(true) }) }) + +describe('testing getCamelCase', () => { + it('properly parses cylinder into cylinder', () => { + expect(getCamelCase('cylinder')).toBe('cylinder') + }) + it('properly parses my-ugly_Cased_Par-123 into myUglyCasedPart', () => { + expect(getCamelCase('my-ugly_Cased_Part123')).toBe('myUglyCasedPart123') + }) + it('properly parses PascalCase into pascalCase', () => { + expect(getCamelCase('PascalCase')).toBe('pascalCase') + }) + it('properly parses my/File/Path into myFilePath', () => { + expect(getCamelCase('my/File/Path')).toBe('myFilePath') + }) +}) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 30dec53865b..ce43b45556e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -473,3 +473,21 @@ export function binaryToUuid( export function getModuleId(sourceRange: SourceRange) { return sourceRange[2] } + +export function getCamelCase(name: string) { + // From https://www.30secondsofcode.org/js/s/string-case-conversion/#word-boundary-identification + const r = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g + const boundaryIdentification = name.match(r) + if (!boundaryIdentification) { + return undefined + } + + const likelyPascalCase = boundaryIdentification + .map((x) => x.slice(0, 1).toUpperCase() + x.slice(1).toLowerCase()) + .join('') + if (!likelyPascalCase) { + return undefined + } + + return likelyPascalCase.slice(0, 1).toLowerCase() + likelyPascalCase.slice(1) +} From b99a12f23df4eaeab178cddf47af5fd42bddf6df Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Tue, 8 Apr 2025 15:19:24 -0400 Subject: [PATCH 26/28] Aligning tests --- src/lib/desktop.test.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lib/desktop.test.ts b/src/lib/desktop.test.ts index cbe1d9644b2..93a3443b829 100644 --- a/src/lib/desktop.test.ts +++ b/src/lib/desktop.test.ts @@ -128,18 +128,18 @@ describe('desktop utilities', () => { // TODO: we should be lowercasing the extension here to check. .sldprt or .SLDPRT should be supported // But the api doesn't allow it today, so revisit this and the tests once this is done - // it('finds supported uppercase extension files relevant', () => { - // expect(isRelevantFile('part.KCL')).toEqual(true) - // expect(isRelevantFile('part.FBX')).toEqual(true) - // expect(isRelevantFile('part.GLTF')).toEqual(true) - // expect(isRelevantFile('part.GLB')).toEqual(true) - // expect(isRelevantFile('part.OBJ')).toEqual(true) - // expect(isRelevantFile('part.PLY')).toEqual(true) - // expect(isRelevantFile('part.SLDPRT')).toEqual(true) - // expect(isRelevantFile('part.STP')).toEqual(true) - // expect(isRelevantFile('part.STEP')).toEqual(true) - // expect(isRelevantFile('part.STL')).toEqual(true) - // }) + it('finds (now) supported uppercase extension files *not* relevant', () => { + expect(isRelevantFile('part.KCL')).toEqual(false) + expect(isRelevantFile('part.FBX')).toEqual(false) + expect(isRelevantFile('part.GLTF')).toEqual(false) + expect(isRelevantFile('part.GLB')).toEqual(false) + expect(isRelevantFile('part.OBJ')).toEqual(false) + expect(isRelevantFile('part.PLY')).toEqual(false) + expect(isRelevantFile('part.SLDPRT')).toEqual(false) + expect(isRelevantFile('part.STP')).toEqual(false) + expect(isRelevantFile('part.STEP')).toEqual(false) + expect(isRelevantFile('part.STL')).toEqual(false) + }) it("doesn't find .docx or .SLDASM relevant", () => { expect(isRelevantFile('paper.docx')).toEqual(false) From 1a538cd2013bb054fd04df5bef71b710fd009a73 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Tue, 8 Apr 2025 15:39:04 -0400 Subject: [PATCH 27/28] Fix tests --- e2e/playwright/point-click-assemblies.spec.ts | 2 +- src/lib/utils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index 4dd906bd7b0..f296ae6e51f 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -192,7 +192,7 @@ test.describe('Point-and-click assemblies tests', () => { const partColor: [number, number, number] = [150, 150, 150] const tolerance = 50 - const complexPlmFileName = 'cube_Complex-PLM_Name_-001.SLDPRT' + const complexPlmFileName = 'cube_Complex-PLM_Name_-001.sldprt' const camelCasedSolidworksFileName = 'cubeComplexPLMName001' await test.step('Setup parts and expect empty assembly scene', async () => { diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 75669b4b9d6..c7d6c53889f 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -1314,7 +1314,7 @@ describe('testing getCamelCase', () => { it('properly parses cylinder into cylinder', () => { expect(getCamelCase('cylinder')).toBe('cylinder') }) - it('properly parses my-ugly_Cased_Par-123 into myUglyCasedPart', () => { + it('properly parses my-ugly_Cased_Part123 into myUglyCasedPart', () => { expect(getCamelCase('my-ugly_Cased_Part123')).toBe('myUglyCasedPart123') }) it('properly parses PascalCase into pascalCase', () => { From 2f94ce7613c8446aa2eb2f1c7bd6915247ac2f97 Mon Sep 17 00:00:00 2001 From: Pierre Jacquier Date: Tue, 8 Apr 2025 16:11:52 -0400 Subject: [PATCH 28/28] Add padding for filenames starting with a digit --- e2e/playwright/point-click-assemblies.spec.ts | 2 +- src/lib/desktop.ts | 6 +++--- src/lib/kclCommands.ts | 4 ++-- src/lib/utils.test.ts | 18 ++++++++++++------ src/lib/utils.ts | 8 ++++++-- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/e2e/playwright/point-click-assemblies.spec.ts b/e2e/playwright/point-click-assemblies.spec.ts index f296ae6e51f..0a61fe9c93d 100644 --- a/e2e/playwright/point-click-assemblies.spec.ts +++ b/e2e/playwright/point-click-assemblies.spec.ts @@ -252,7 +252,7 @@ test.describe('Point-and-click assemblies tests', () => { // Go through the ToastInsert prompt await page.getByText('Insert into my current file').click() - // Check getCamelCaseFromFileName output + // Check getPathFilenameInVariableCase output const parsedValueFromFile = await cmdBar.currentArgumentInput.inputValue() expect(parsedValueFromFile).toEqual(camelCasedSolidworksFileName) diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index 61feaeb0d80..352d61b7515 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -25,7 +25,7 @@ import { import type { FileEntry, Project } from '@src/lib/project' import { err } from '@src/lib/trap' import type { DeepPartial } from '@src/lib/types' -import { getCamelCase } from '@src/lib/utils' +import { getInVariableCase } from '@src/lib/utils' export async function renameProjectDirectory( projectPath: string, @@ -728,11 +728,11 @@ export const writeProjectThumbnailFile = async ( return window.electron.writeFile(filePath, asArray) } -export function getCamelCaseFromFilePath(path: string) { +export function getPathFilenameInVariableCase(path: string) { // from https://nodejs.org/en/learn/manipulating-files/nodejs-file-paths#example const basenameNoExt = window.electron.path.basename( path, window.electron.path.extname(path) ) - return getCamelCase(basenameNoExt) + return getInVariableCase(basenameNoExt) } diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index 0233d8142da..e31745ffcde 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -17,7 +17,7 @@ import { EXECUTION_TYPE_REAL, FILE_EXT, } from '@src/lib/constants' -import { getCamelCaseFromFilePath } from '@src/lib/desktop' +import { getPathFilenameInVariableCase } from '@src/lib/desktop' import { isDesktop } from '@src/lib/isDesktop' import { copyFileShareLink } from '@src/lib/links' import { baseUnitsUnion } from '@src/lib/settings/settingsTypes' @@ -130,7 +130,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { } const path = context.argumentsToSubmit['path'] as string - return getCamelCaseFromFilePath(path) + return getPathFilenameInVariableCase(path) }, }, }, diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index c7d6c53889f..cf96a842361 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -1,7 +1,7 @@ import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange' import { topLevelRange } from '@src/lang/util' import { - getCamelCase, + getInVariableCase, hasDigitsLeftOfDecimal, hasLeadingZero, isClockwise, @@ -1310,17 +1310,23 @@ describe('testing isClockwise', () => { }) }) -describe('testing getCamelCase', () => { +describe('testing getInVariableCase', () => { it('properly parses cylinder into cylinder', () => { - expect(getCamelCase('cylinder')).toBe('cylinder') + expect(getInVariableCase('cylinder')).toBe('cylinder') }) it('properly parses my-ugly_Cased_Part123 into myUglyCasedPart', () => { - expect(getCamelCase('my-ugly_Cased_Part123')).toBe('myUglyCasedPart123') + expect(getInVariableCase('my-ugly_Cased_Part123')).toBe( + 'myUglyCasedPart123' + ) }) it('properly parses PascalCase into pascalCase', () => { - expect(getCamelCase('PascalCase')).toBe('pascalCase') + expect(getInVariableCase('PascalCase')).toBe('pascalCase') }) it('properly parses my/File/Path into myFilePath', () => { - expect(getCamelCase('my/File/Path')).toBe('myFilePath') + expect(getInVariableCase('my/File/Path')).toBe('myFilePath') + }) + it('properly parses prefixes 1120t74-pipe.step', () => { + expect(getInVariableCase('1120t74-pipe')).toBe('m1120T74Pipe') + expect(getInVariableCase('1120t74-pipe', 'p')).toBe('p1120T74Pipe') }) }) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ce43b45556e..ab510466b5d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -474,10 +474,14 @@ export function getModuleId(sourceRange: SourceRange) { return sourceRange[2] } -export function getCamelCase(name: string) { +export function getInVariableCase(name: string, prefixIfDigit = 'm') { + // As of 2025-04-08, standard case for KCL variables is camelCase + const startsWithANumber = !Number.isNaN(Number(name.charAt(0))) + const paddedName = startsWithANumber ? `${prefixIfDigit}${name}` : name + // From https://www.30secondsofcode.org/js/s/string-case-conversion/#word-boundary-identification const r = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g - const boundaryIdentification = name.match(r) + const boundaryIdentification = paddedName.match(r) if (!boundaryIdentification) { return undefined }