Add point-and-click Insert from local project files (#6129)
* 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 * Better pathToNOde, log on non-working cm dispatch call * Add workaround to updateModelingState not working * Back to updateModelingState with a skip flag * Better todo * Change working from Import to Insert, cleanups * Sister command in kclCommands to populate file options * Improve path selector * Unsure: move importAstMod to kclCommands onSubmit 😶 * Add e2e test * Clean up for review * Add native file menu entry and test * No await yo lint said so * @lrev-Dev's suggestion to remove a comment Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Update to scene.settled(cmdBar) * Lint --------- Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
@ -44,6 +44,7 @@ export class ToolbarFixture {
|
|||||||
featureTreePane!: Locator
|
featureTreePane!: Locator
|
||||||
gizmo!: Locator
|
gizmo!: Locator
|
||||||
gizmoDisabled!: Locator
|
gizmoDisabled!: Locator
|
||||||
|
insertButton!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -78,6 +79,8 @@ export class ToolbarFixture {
|
|||||||
// element or two different elements can represent these states.
|
// element or two different elements can represent these states.
|
||||||
this.gizmo = page.getByTestId('gizmo')
|
this.gizmo = page.getByTestId('gizmo')
|
||||||
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
||||||
|
|
||||||
|
this.insertButton = page.getByTestId('insert-pane-button')
|
||||||
}
|
}
|
||||||
|
|
||||||
get logoLink() {
|
get logoLink() {
|
||||||
|
@ -570,6 +570,43 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
const expected = 'Open sample'
|
const expected = 'Open sample'
|
||||||
expect(actual).toBe(expected)
|
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.settled(cmdBar)
|
||||||
|
|
||||||
|
// 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 ({
|
test('Modeling.File.Export current part', async ({
|
||||||
tronApp,
|
tronApp,
|
||||||
cmdBar,
|
cmdBar,
|
||||||
|
115
e2e/playwright/point-click-assemblies.spec.ts
Normal file
115
e2e/playwright/point-click-assemblies.spec.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
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.settled(cmdBar)
|
||||||
|
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 cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: { Path: 'cylinder.kcl', LocalName: 'cylinder' },
|
||||||
|
commandName: 'Insert',
|
||||||
|
})
|
||||||
|
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 cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: { Path: 'bracket.kcl', LocalName: 'bracket' },
|
||||||
|
commandName: 'Insert',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
import "cylinder.kcl" as cylinder
|
||||||
|
import "bracket.kcl" as bracket
|
||||||
|
cylinder
|
||||||
|
bracket
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
@ -459,10 +459,30 @@ export const FileMachineProvider = ({
|
|||||||
name: sample.title,
|
name: sample.title,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
specialPropsForInsertCommand: {
|
||||||
|
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,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
const isDirectory = v.children
|
||||||
|
const isCurrentFile = v.path === file?.path
|
||||||
|
return isDirectory || isCurrentFile
|
||||||
|
? []
|
||||||
|
: {
|
||||||
|
name: relativeFilePath,
|
||||||
|
value: relativeFilePath,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
}).filter(
|
}).filter(
|
||||||
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
||||||
),
|
),
|
||||||
[codeManager, kclManager, send, kclSamples]
|
[codeManager, kclManager, send, kclSamples, project, file]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -14,6 +14,7 @@ import type {
|
|||||||
} from '@src/components/ModelingSidebar/ModelingPanes'
|
} from '@src/components/ModelingSidebar/ModelingPanes'
|
||||||
import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes'
|
import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes'
|
||||||
import Tooltip from '@src/components/Tooltip'
|
import Tooltip from '@src/components/Tooltip'
|
||||||
|
import { DEV } from '@src/env'
|
||||||
import { useModelingContext } from '@src/hooks/useModelingContext'
|
import { useModelingContext } from '@src/hooks/useModelingContext'
|
||||||
import { useKclContext } from '@src/lang/KclProvider'
|
import { useKclContext } from '@src/lang/KclProvider'
|
||||||
import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
|
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 { useSettings } from '@src/machines/appMachine'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -60,6 +62,19 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const sidebarActions: SidebarAction[] = [
|
const sidebarActions: SidebarAction[] = [
|
||||||
|
{
|
||||||
|
id: 'insert',
|
||||||
|
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',
|
||||||
|
data: { name: 'Insert', groupId: 'code' },
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'export',
|
id: 'export',
|
||||||
title: 'Export part',
|
title: 'Export part',
|
||||||
|
@ -113,6 +113,7 @@ function ProjectMenuPopover({
|
|||||||
const commands = useSelector(commandBarActor, commandsSelector)
|
const commands = useSelector(commandBarActor, commandsSelector)
|
||||||
|
|
||||||
const { onProjectClose } = useLspContext()
|
const { onProjectClose } = useLspContext()
|
||||||
|
const insertCommandInfo = { name: 'Insert', groupId: 'code' }
|
||||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||||
const shareCommandInfo = { name: 'share-file-link', groupId: 'code' }
|
const shareCommandInfo = { name: 'share-file-link', groupId: 'code' }
|
||||||
@ -145,6 +146,29 @@ function ProjectMenuPopover({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'break',
|
'break',
|
||||||
|
{
|
||||||
|
id: 'insert',
|
||||||
|
Element: 'button',
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<span>Insert from project file</span>
|
||||||
|
{!findCommand(insertCommandInfo) && (
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
wrapperClassName="!max-w-none min-w-fit"
|
||||||
|
>
|
||||||
|
Awaiting engine connection
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
disabled: !findCommand(insertCommandInfo),
|
||||||
|
onClick: () =>
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: insertCommandInfo,
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'export',
|
id: 'export',
|
||||||
Element: 'button',
|
Element: 'button',
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
|
||||||
import type { Name } from '@rust/kcl-lib/bindings/Name'
|
import type { Name } from '@rust/kcl-lib/bindings/Name'
|
||||||
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
import type { TagDeclarator } from '@rust/kcl-lib/bindings/TagDeclarator'
|
import type { TagDeclarator } from '@rust/kcl-lib/bindings/TagDeclarator'
|
||||||
|
|
||||||
|
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 { ARG_TAG } from '@src/lang/constants'
|
||||||
import { getNodeFromPath } from '@src/lang/queryAst'
|
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||||
@ -12,6 +16,7 @@ import type {
|
|||||||
CallExpression,
|
CallExpression,
|
||||||
CallExpressionKw,
|
CallExpressionKw,
|
||||||
Expr,
|
Expr,
|
||||||
|
ExpressionStatement,
|
||||||
Identifier,
|
Identifier,
|
||||||
LabeledArg,
|
LabeledArg,
|
||||||
Literal,
|
Literal,
|
||||||
@ -333,6 +338,44 @@ export function createBinaryExpressionWithUnary([left, right]: [
|
|||||||
return createBinaryExpression([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 = 'default'
|
||||||
|
): Node<ImportStatement> {
|
||||||
|
return {
|
||||||
|
type: 'ImportStatement',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
outerAttrs: [],
|
||||||
|
preComments: [],
|
||||||
|
commentStart: 0,
|
||||||
|
selector,
|
||||||
|
path,
|
||||||
|
visibility,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExpressionStatement(
|
||||||
|
expression: Expr
|
||||||
|
): Node<ExpressionStatement> {
|
||||||
|
return {
|
||||||
|
type: 'ExpressionStatement',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
outerAttrs: [],
|
||||||
|
preComments: [],
|
||||||
|
commentStart: 0,
|
||||||
|
expression,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function findUniqueName(
|
export function findUniqueName(
|
||||||
ast: Program | string,
|
ast: Program | string,
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
EXECUTION_TYPE_NONE,
|
EXECUTION_TYPE_NONE,
|
||||||
EXECUTION_TYPE_REAL,
|
EXECUTION_TYPE_REAL,
|
||||||
} from '@src/lib/constants'
|
} from '@src/lib/constants'
|
||||||
|
import type { Selections } from '@src/lib/selections'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the complete modeling state:
|
* Updates the complete modeling state:
|
||||||
@ -52,15 +53,23 @@ export async function updateModelingState(
|
|||||||
},
|
},
|
||||||
options?: {
|
options?: {
|
||||||
focusPath?: Array<PathToNode>
|
focusPath?: Array<PathToNode>
|
||||||
|
skipUpdateAst?: boolean
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
let updatedAst: {
|
||||||
|
newAst: Node<Program>
|
||||||
|
selections?: Selections
|
||||||
|
} = { newAst: ast }
|
||||||
|
// 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)
|
// Step 1: Update AST without executing (prepare selections)
|
||||||
const updatedAst = await dependencies.kclManager.updateAst(
|
updatedAst = await dependencies.kclManager.updateAst(
|
||||||
ast,
|
ast,
|
||||||
// false == mock execution. Is this what we want?
|
|
||||||
false, // Execution handled separately for error resilience
|
false, // Execution handled separately for error resilience
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2: Update the code editor and save file
|
// Step 2: Update the code editor and save file
|
||||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
@ -9,7 +9,10 @@ import {
|
|||||||
createArrayExpression,
|
createArrayExpression,
|
||||||
createCallExpressionStdLib,
|
createCallExpressionStdLib,
|
||||||
createCallExpressionStdLibKw,
|
createCallExpressionStdLibKw,
|
||||||
|
createExpressionStatement,
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
|
createImportAsSelector,
|
||||||
|
createImportStatement,
|
||||||
createLabeledArg,
|
createLabeledArg,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
createLocalName,
|
createLocalName,
|
||||||
@ -778,6 +781,57 @@ export function addOffsetPlane({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an import call to load a part
|
||||||
|
*/
|
||||||
|
export function addImportAndInsert({
|
||||||
|
node,
|
||||||
|
path,
|
||||||
|
localName,
|
||||||
|
}: {
|
||||||
|
node: Node<Program>
|
||||||
|
path: string
|
||||||
|
localName: string
|
||||||
|
}): {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToImportNode: PathToNode
|
||||||
|
pathToInsertNode: PathToNode
|
||||||
|
} {
|
||||||
|
const modifiedAst = structuredClone(node)
|
||||||
|
|
||||||
|
// Add import statement
|
||||||
|
const importStatement = createImportStatement(
|
||||||
|
createImportAsSelector(localName),
|
||||||
|
{ type: 'Kcl', filename: path }
|
||||||
|
)
|
||||||
|
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'],
|
||||||
|
['path', 'ImportStatement'],
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add insert statement
|
||||||
|
const insertStatement = createExpressionStatement(createLocalName(localName))
|
||||||
|
const insertIndex = modifiedAst.body.length
|
||||||
|
modifiedAst.body.push(insertStatement)
|
||||||
|
const pathToInsertNode: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[insertIndex, 'index'],
|
||||||
|
['expression', 'ExpressionStatement'],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
pathToImportNode,
|
||||||
|
pathToInsertNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append a helix to the AST
|
* Append a helix to the AST
|
||||||
*/
|
*/
|
||||||
|
@ -2,6 +2,9 @@ import type { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning'
|
import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning'
|
||||||
|
import { DEV } from '@src/env'
|
||||||
|
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||||
|
import { addImportAndInsert } from '@src/lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
changeKclSettings,
|
changeKclSettings,
|
||||||
unitAngleToUnitAng,
|
unitAngleToUnitAng,
|
||||||
@ -11,14 +14,16 @@ import type { Command, CommandArgumentOption } from '@src/lib/commandTypes'
|
|||||||
import {
|
import {
|
||||||
DEFAULT_DEFAULT_ANGLE_UNIT,
|
DEFAULT_DEFAULT_ANGLE_UNIT,
|
||||||
DEFAULT_DEFAULT_LENGTH_UNIT,
|
DEFAULT_DEFAULT_LENGTH_UNIT,
|
||||||
|
EXECUTION_TYPE_REAL,
|
||||||
FILE_EXT,
|
FILE_EXT,
|
||||||
} from '@src/lib/constants'
|
} from '@src/lib/constants'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { copyFileShareLink } from '@src/lib/links'
|
import { copyFileShareLink } from '@src/lib/links'
|
||||||
import { baseUnitsUnion } from '@src/lib/settings/settingsTypes'
|
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 { err, reportRejection } from '@src/lib/trap'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
|
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
||||||
|
|
||||||
interface OnSubmitProps {
|
interface OnSubmitProps {
|
||||||
sampleName: string
|
sampleName: string
|
||||||
@ -34,6 +39,9 @@ interface KclCommandConfig {
|
|||||||
onSubmit: (p: OnSubmitProps) => Promise<void>
|
onSubmit: (p: OnSubmitProps) => Promise<void>
|
||||||
providedOptions: CommandArgumentOption<string>[]
|
providedOptions: CommandArgumentOption<string>[]
|
||||||
}
|
}
|
||||||
|
specialPropsForInsertCommand: {
|
||||||
|
providedOptions: CommandArgumentOption<string>[]
|
||||||
|
}
|
||||||
projectData: IndexLoaderData
|
projectData: IndexLoaderData
|
||||||
authToken: string
|
authToken: string
|
||||||
settings: {
|
settings: {
|
||||||
@ -96,6 +104,50 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Insert',
|
||||||
|
description: 'Insert from a file in the current project directory',
|
||||||
|
icon: 'import',
|
||||||
|
groupId: 'code',
|
||||||
|
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',
|
||||||
|
required: true,
|
||||||
|
options: commandProps.specialPropsForInsertCommand.providedOptions,
|
||||||
|
},
|
||||||
|
localName: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onSubmit: (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,
|
||||||
|
})
|
||||||
|
updateModelingState(
|
||||||
|
modifiedAst,
|
||||||
|
EXECUTION_TYPE_REAL,
|
||||||
|
{ kclManager, editorManager, codeManager },
|
||||||
|
{
|
||||||
|
skipUpdateAst: true,
|
||||||
|
focusPath: [pathToImportNode, pathToInsertNode],
|
||||||
|
}
|
||||||
|
).catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'format-code',
|
name: 'format-code',
|
||||||
displayName: 'Format Code',
|
displayName: 'Format Code',
|
||||||
|
@ -25,6 +25,7 @@ export type MenuLabels =
|
|||||||
| 'File.Create new file'
|
| 'File.Create new file'
|
||||||
| 'File.Create new folder'
|
| 'File.Create new folder'
|
||||||
| 'File.Load a sample model'
|
| 'File.Load a sample model'
|
||||||
|
| 'File.Insert from project file'
|
||||||
| 'File.Export current part'
|
| 'File.Export current part'
|
||||||
| 'File.Share current part (via Zoo link)'
|
| 'File.Share current part (via Zoo link)'
|
||||||
| 'File.Preferences.Project settings'
|
| 'File.Preferences.Project settings'
|
||||||
|
@ -157,6 +157,15 @@ export const modelingFileRole = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ 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',
|
label: 'Export current part',
|
||||||
id: 'File.Export current part',
|
id: 'File.Export current part',
|
||||||
|
@ -92,6 +92,14 @@ export function modelingMenuCallbackMostActions(
|
|||||||
}).catch(reportRejection)
|
}).catch(reportRejection)
|
||||||
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
||||||
navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit')
|
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') {
|
} else if (data.menuLabel === 'File.Export current part') {
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
|
@ -21,6 +21,7 @@ type FileRoleLabel =
|
|||||||
| 'Sign out'
|
| 'Sign out'
|
||||||
| 'Theme'
|
| 'Theme'
|
||||||
| 'Theme color'
|
| 'Theme color'
|
||||||
|
| 'Insert from project file'
|
||||||
| 'Export current part'
|
| 'Export current part'
|
||||||
| 'Create new file'
|
| 'Create new file'
|
||||||
| 'Create new folder'
|
| 'Create new folder'
|
||||||
|
Reference in New Issue
Block a user