Compare commits

..

1 Commits

Author SHA1 Message Date
4322d3633e Revert "Add uniqueness check to "Create project" command (#5100)"
This reverts commit dac91d3b79.
2025-01-17 15:15:24 -05:00
288 changed files with 70160 additions and 100898 deletions

View File

@ -5,7 +5,6 @@ on:
push: push:
branches: branches:
- main - main
- pierremtb/4088/create-file-url
tags: tags:
- 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+'
schedule: schedule:

44
.github/workflows/cargo-bench.yml vendored Normal file
View File

@ -0,0 +1,44 @@
on:
push:
branches:
- main
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
pull_request:
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo bench
jobs:
cargo-bench:
name: Benchmark with iai
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
cargo install cargo-criterion
sudo apt update
sudo apt install -y valgrind
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library
shell: bash
run: |-
cd src/wasm-lib/kcl; cargo bench --all-features -- iai
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
---
title: "Face"
excerpt: "A face."
layout: manual
---
A face.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |`string`| The id of the face. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -20,7 +20,6 @@ A helix.
| `revolutions` |`number`| Number of revolutions. | No | | `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No | | `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -20,7 +20,6 @@ A helix.
| `revolutions` |`number`| Number of revolutions. | No | | `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No | | `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -168,6 +168,7 @@ Any KCL value.
---- ----
A plane.
**Type:** `object` **Type:** `object`
@ -180,10 +181,17 @@ Any KCL value.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No | | `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No | | `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
---- ----
A face.
**Type:** `object` **Type:** `object`
@ -195,8 +203,14 @@ Any KCL value.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No | | `type` |enum: `Face`| | No |
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No | | `id` |`string`| The id of the face. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
---- ----
@ -232,6 +246,7 @@ Any KCL value.
---- ----
An solid is a collection of extrude surfaces.
**Type:** `object` **Type:** `object`
@ -244,7 +259,14 @@ Any KCL value.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No | | `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No | | `id` |`string`| The id of the solid. | No |
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
| `height` |`number`| The height of the solid. | No |
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
---- ----
@ -264,6 +286,7 @@ Any KCL value.
---- ----
A helix.
**Type:** `object` **Type:** `object`
@ -276,7 +299,11 @@ Any KCL value.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No | | `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No | | `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
---- ----

View File

@ -22,7 +22,6 @@ A plane.
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -21,7 +21,6 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -30,7 +30,6 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -31,7 +31,6 @@ A plane.
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
@ -55,7 +54,6 @@ A face.
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -23,7 +23,6 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No | | `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No | | `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -32,7 +32,6 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No | | `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No | | `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -1,107 +0,0 @@
---
title: "UnitLen"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Mm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Cm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `M`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Inches`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Feet`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Yards`| | No |
----

View File

@ -280,7 +280,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await test.step('Opening the router-template project should load', async () => { await test.step('Opening the router-template project should load', async () => {

View File

@ -1,8 +1,7 @@
import { test, expect } from './zoo-test' import { test, expect } from './zoo-test'
import * as fsp from 'fs/promises'
import { executorInputPath, getUtils } from './test-utils' import { getUtils } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import path from 'path'
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({ test('Extrude from command bar selects extrude line after', async ({
@ -306,132 +305,4 @@ test.describe('Command bar tests', () => {
await arcToolCommand.click() await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true') await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
}) })
test(`Reacts to query param to open "import from URL" command`, async ({
page,
cmdBar,
editor,
homePage,
}) => {
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'New Project' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'New project',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
})
})
test(`"import from URL" can add to existing project`, async ({
page,
cmdBar,
editor,
homePage,
toolbar,
context,
}) => {
await context.folderSetupFn(async (dir) => {
const testProjectDir = path.join(dir, 'testProjectDir')
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('cylinder.kcl'),
path.join(testProjectDir, 'main.kcl')
),
])
})
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [
{
fileCount: 1,
title: 'testProjectDir',
},
],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'Existing Project' }).click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'projectName',
currentArgValue: '',
headerArguments: {
Method: 'Existing project',
Name: 'test',
ProjectName: '',
Code: '1 line',
},
highlightedHeaderArg: 'projectName',
})
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'Existing project',
ProjectName: 'testProjectDir',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
await toolbar.openPane('files')
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
})
})
}) })

View File

@ -135,27 +135,4 @@ export class CmdBarFixture {
await promptEditCommand.first().click() await promptEditCommand.first().click()
} }
} }
get cmdSearchInput() {
return this.page.getByTestId('cmd-bar-search')
}
get argumentInput() {
return this.page.getByTestId('cmd-bar-arg-value')
}
get cmdOptions() {
return this.page.getByTestId('cmd-bar-option')
}
chooseCommand = async (commandName: string) => {
await this.cmdOptions.getByText(commandName).click()
}
/**
* Select an option from the command bar
*/
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
return this.page.getByRole('option', options)
}
} }

View File

@ -103,7 +103,7 @@ export class HomePageFixture {
.toEqual(expectedState) .toEqual(expectedState)
} }
createAndGoToProject = async (projectTitle = 'project-$nnn') => { createAndGoToProject = async (projectTitle: string) => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...') await expect(this.projectSection).not.toHaveText('Loading your Projects...')
await this.projectButtonNew.click() await this.projectButtonNew.click()
await this.projectTextName.click() await this.projectTextName.click()

View File

@ -63,10 +63,6 @@ export class ToolbarFixture {
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
} }
get logoLink() {
return this.page.getByTestId('app-logo')
}
startSketchPlaneSelection = async () => startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)

View File

@ -172,7 +172,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible() await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await test.step('opening broken code project should clear the scene and show the error', async () => { await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home. // Go back home.
@ -253,7 +253,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible() await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await test.step('opening empty code project should clear the scene', async () => { await test.step('opening empty code project should clear the scene', async () => {
// Go back home. // Go back home.
@ -985,126 +985,6 @@ test.describe(`Project management commands`, () => {
}) })
} }
) )
test(`Create a new project with a colliding name`, async ({
context,
homePage,
toolbar,
cmdBar,
}) => {
const projectName = 'test-project'
await test.step(`Setup`, async () => {
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, projectName)
await Promise.all([fsp.mkdir(projectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
path.join(projectDir, 'main.kcl')
),
])
})
await homePage.expectState({
projectCards: [
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create a new project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create another project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the second project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-2',
fileCount: 1,
},
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
})
})
test(`Create a few projects using the default project name`, async ({
homePage,
toolbar,
}) => {
for (let i = 0; i < 12; i++) {
await test.step(`Create project ${i}`, async () => {
await homePage.expectState({
projectCards: Array.from({ length: i }, (_, i) => ({
title: `project-${i.toString().padStart(3, '0')}`,
fileCount: 1,
})).toReversed(),
sortBy: 'last-modified-desc',
})
await homePage.createAndGoToProject()
await toolbar.logoLink.click()
})
}
}) })
test( test(
@ -1511,7 +1391,7 @@ extrude001 = extrude(200, sketch001)`)
await page.getByTestId('app-logo').click() await page.getByTestId('app-logo').click()
await expect( await expect(
page.getByRole('button', { name: 'Create project' }) page.getByRole('button', { name: 'New project' })
).toBeVisible() ).toBeVisible()
for (let i = 1; i <= 10; i++) { for (let i = 1; i <= 10; i++) {
@ -1585,7 +1465,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await test.step('Opening the router-template project should load the stream', async () => { await test.step('Opening the router-template project should load the stream', async () => {
@ -1614,7 +1494,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
} }
) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1078,7 +1078,7 @@ export async function createProject({
returnHome?: boolean returnHome?: boolean
}) { }) {
await test.step(`Create project and navigate to it`, async () => { await test.step(`Create project and navigate to it`, async () => {
await page.getByRole('button', { name: 'Create project' }).click() await page.getByRole('button', { name: 'New project' }).click()
await page.getByRole('textbox', { name: 'Name' }).fill(name) await page.getByRole('textbox', { name: 'Name' }).fill(name)
await page.getByRole('button', { name: 'Continue' }).click() await page.getByRole('button', { name: 'Continue' }).click()

View File

@ -22,8 +22,6 @@ import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu' import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle' import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { maybeWriteToDisk } from 'lib/telemetry' import { maybeWriteToDisk } from 'lib/telemetry'
maybeWriteToDisk() maybeWriteToDisk()
.then(() => {}) .then(() => {})
@ -31,20 +29,6 @@ maybeWriteToDisk()
export function App() { export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
const { commandBarSend } = useCommandsContext()
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery((argDefaultValues) => {
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'projects',
name: 'Import file from URL',
argDefaultValues,
},
})
})
useRefreshSettings(PATHS.FILE + 'SETTINGS') useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()

View File

@ -35,7 +35,7 @@ import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
import SettingsAuthProvider from 'components/SettingsAuthProvider' import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider' import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider' import { KclContextProvider } from 'lang/KclProvider'
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants' import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -47,7 +47,6 @@ import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider' import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider' import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -59,7 +58,6 @@ const router = createRouter([
/* Make sure auth is the outermost provider or else we will have /* Make sure auth is the outermost provider or else we will have
* inefficient re-renders, use the react profiler to see. */ * inefficient re-renders, use the react profiler to see. */
element: ( element: (
<OpenInDesktopAppHandler>
<CommandBarProvider> <CommandBarProvider>
<RouteProvider> <RouteProvider>
<SettingsAuthProvider> <SettingsAuthProvider>
@ -77,26 +75,16 @@ const router = createRouter([
</SettingsAuthProvider> </SettingsAuthProvider>
</RouteProvider> </RouteProvider>
</CommandBarProvider> </CommandBarProvider>
</OpenInDesktopAppHandler>
), ),
errorElement: <ErrorPage />, errorElement: <ErrorPage />,
children: [ children: [
{ {
path: PATHS.INDEX, path: PATHS.INDEX,
loader: async ({ request }) => { loader: async () => {
const onDesktop = isDesktop() const onDesktop = isDesktop()
const url = new URL(request.url) return onDesktop
if (onDesktop) { ? redirect(PATHS.HOME)
return redirect(PATHS.HOME + (url.search || '')) : redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
} else {
const searchParams = new URLSearchParams(url.search)
if (!searchParams.has(ASK_TO_OPEN_QUERY_PARAM)) {
return redirect(
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
)
}
}
return null
}, },
}, },
{ {

View File

@ -1,4 +1,4 @@
import { useRef, useMemo, memo, useCallback, useState } from 'react' import { useRef, useMemo, memo } from 'react'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -34,7 +34,8 @@ export function Toolbar({
const bgClassName = '!bg-transparent' const bgClassName = '!bg-transparent'
const buttonBgClassName = const buttonBgClassName =
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10' 'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
const buttonBorderClassName = '!border-transparent' const buttonBorderClassName =
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const sketchPathId = useMemo(() => { const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
@ -49,7 +50,6 @@ export function Toolbar({
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState() const { isStreamReady } = useAppState()
const [showRichContent, setShowRichContent] = useState(false)
const disableAllButtons = const disableAllButtons =
(overallState !== NetworkHealthState.Ok && (overallState !== NetworkHealthState.Ok &&
@ -77,40 +77,6 @@ export function Toolbar({
[state, send, commandBarSend, sketchPathId] [state, send, commandBarSend, sketchPathId]
) )
const tooltipContentClassName = !showRichContent
? ''
: '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch'
const richContentTimeout = useRef<number | null>(null)
const richContentClearTimeout = useRef<number | null>(null)
// On mouse enter, show rich content after a 1s delay
const handleMouseEnter = useCallback(() => {
// Cancel the clear timeout if it's already set
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
// Start our own timeout to show the rich content
richContentTimeout.current = window.setTimeout(() => {
setShowRichContent(true)
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
}, 1000)
}, [setShowRichContent])
// On mouse leave, clear the timeout and hide rich content
const handleMouseLeave = useCallback(() => {
// Clear the timeout to show rich content
if (richContentTimeout.current) {
clearTimeout(richContentTimeout.current)
}
// Start a timeout to hide the rich content
richContentClearTimeout.current = window.setTimeout(() => {
setShowRichContent(false)
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
}, 500)
}, [setShowRichContent])
/** /**
* Resolve all the callbacks and values for the current mode, * Resolve all the callbacks and values for the current mode,
* so we don't need to worry about the other modes * so we don't need to worry about the other modes
@ -207,12 +173,6 @@ export function Toolbar({
itemConfig.disabled === true, itemConfig.disabled === true,
status: itemConfig.status, status: itemConfig.status,
}))} }))}
>
<div
className="contents"
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
> >
<ActionButton <ActionButton
Element="button" Element="button"
@ -246,26 +206,11 @@ export function Toolbar({
> >
{maybeIconConfig[0].title} {maybeIconConfig[0].title}
</span> </span>
</ActionButton>
<ToolbarItemTooltip <ToolbarItemTooltip
itemConfig={maybeIconConfig[0]} itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps} configCallbackProps={configCallbackProps}
wrapperClassName="ui-open:!hidden"
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent
itemConfig={maybeIconConfig[0]}
/> />
) : (
<ToolbarItemTooltipShortContent
status={maybeIconConfig[0].status}
title={maybeIconConfig[0].title}
hotkey={maybeIconConfig[0].hotkey}
/>
)}
</ToolbarItemTooltip>
</ActionButton>
</div>
</ActionButtonDropdown> </ActionButtonDropdown>
) )
} }
@ -273,13 +218,7 @@ export function Toolbar({
// A single button // A single button
return ( return (
<div <div className="relative" key={itemConfig.id}>
className="relative"
key={itemConfig.id}
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<ActionButton <ActionButton
Element="button" Element="button"
key={itemConfig.id} key={itemConfig.id}
@ -316,18 +255,7 @@ export function Toolbar({
<ToolbarItemTooltip <ToolbarItemTooltip
itemConfig={itemConfig} itemConfig={itemConfig}
configCallbackProps={configCallbackProps} configCallbackProps={configCallbackProps}
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
) : (
<ToolbarItemTooltipShortContent
status={itemConfig.status}
title={itemConfig.title}
hotkey={itemConfig.hotkey}
/> />
)}
</ToolbarItemTooltip>
</div> </div>
) )
})} })}
@ -341,12 +269,6 @@ export function Toolbar({
) )
} }
interface ToolbarItemContentsProps extends React.PropsWithChildren {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
wrapperClassName?: string
contentClassName?: string
}
/** /**
* The single button and dropdown button share content, so we extract it here * The single button and dropdown button share content, so we extract it here
* It contains a tooltip with the title, description, and links * It contains a tooltip with the title, description, and links
@ -355,10 +277,12 @@ interface ToolbarItemContentsProps extends React.PropsWithChildren {
const ToolbarItemTooltip = memo(function ToolbarItemContents({ const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig, itemConfig,
configCallbackProps, configCallbackProps,
wrapperClassName = '', }: {
contentClassName = '', itemConfig: ToolbarItemResolved
children, configCallbackProps: ToolbarItemCallbackProps
}: ToolbarItemContentsProps) { }) {
const { state } = useModelingContext()
useHotkeys( useHotkeys(
itemConfig.hotkey || '', itemConfig.hotkey || '',
() => { () => {
@ -381,50 +305,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties) ? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {} : {}
} }
hoverOnly
position="bottom" position="bottom"
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName} wrapperClassName="!p-4 !pointer-events-auto"
contentClassName={contentClassName} contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
delay={0}
> >
{children}
</Tooltip>
)
})
const ToolbarItemTooltipShortContent = ({
status,
title,
hotkey,
}: {
status: string
title: string
hotkey?: string | string[]
}) => (
<span
className={`text-sm ${
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
}`}
>
{title}
{hotkey && (
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
)}
</span>
)
const ToolbarItemTooltipRichContent = ({
itemConfig,
}: {
itemConfig: ToolbarItemResolved
}) => {
const { state } = useModelingContext()
return (
<>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50"> <div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
{itemConfig.icon && (
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
)}
<span <span
className={`text-sm flex-1 ${ className={`text-sm flex-1 ${
itemConfig.status !== 'available' itemConfig.status !== 'available'
@ -493,6 +378,6 @@ const ToolbarItemTooltipRichContent = ({
</ul> </ul>
</> </>
)} )}
</> </Tooltip>
) )
} })

View File

@ -1398,23 +1398,23 @@ export class SceneEntities {
const arg0 = arg(kclCircle3PointArgs[0]) const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast if (!arg0) return kclManager.ast
arg0[0].value = { value: points[0].x, suffix: 'None' } arg0[0].value = points[0].x
arg0[0].raw = points[0].x.toString() arg0[0].raw = points[0].x.toString()
arg0[1].value = { value: points[0].y, suffix: 'None' } arg0[1].value = points[0].y
arg0[1].raw = points[0].y.toString() arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1]) const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast if (!arg1) return kclManager.ast
arg1[0].value = { value: points[1].x, suffix: 'None' } arg1[0].value = points[1].x
arg1[0].raw = points[1].x.toString() arg1[0].raw = points[1].x.toString()
arg1[1].value = { value: points[1].y, suffix: 'None' } arg1[1].value = points[1].y
arg1[1].raw = points[1].y.toString() arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2]) const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast if (!arg2) return kclManager.ast
arg2[0].value = { value: points[2].x, suffix: 'None' } arg2[0].value = points[2].x
arg2[0].raw = points[2].x.toString() arg2[0].raw = points[2].x.toString()
arg2[1].value = { value: points[2].y, suffix: 'None' } arg2[1].value = points[2].y
arg2[1].raw = points[2].y.toString() arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast) const astSnapshot = structuredClone(kclManager.ast)
@ -2051,8 +2051,8 @@ export class SceneEntities {
) )
if (!(sk instanceof Reason)) { if (!(sk instanceof Reason)) {
sketch = sk sketch = sk
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) { } else if ((maybeSketch as Solid).sketch) {
sketch = (maybeSketch.value as Solid).sketch sketch = (maybeSketch as Solid).sketch
} }
if (!sketch) return if (!sketch) return
@ -2541,7 +2541,7 @@ export function sketchFromPathToNode({
const varDec = _varDec.node const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '') const result = programMemory.get(varDec?.id?.name || '')
if (result?.type === 'Solid') { if (result?.type === 'Solid') {
return result.value.sketch return result.sketch
} }
const sg = sketchFromKclValue(result, varDec?.id?.name) const sg = sketchFromKclValue(result, varDec?.id?.name)
if (err(sg)) { if (err(sg)) {

View File

@ -1,11 +1,9 @@
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { ActionButtonProps } from './ActionButton' import { ActionButtonProps } from './ActionButton'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import Tooltip from './Tooltip'
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & { type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
name?: string name?: string
dropdownTooltipText?: string
splitMenuItems: { splitMenuItems: {
id: string id: string
label: string label: string
@ -19,7 +17,6 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
export function ActionButtonDropdown({ export function ActionButtonDropdown({
splitMenuItems, splitMenuItems,
className, className,
dropdownTooltipText = 'More tools',
children, children,
...props ...props
}: ActionButtonSplitProps) { }: ActionButtonSplitProps) {
@ -29,14 +26,7 @@ export function ActionButtonDropdown({
{({ close }) => ( {({ close }) => (
<> <>
{children} {children}
<Popover.Button <Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
className={
'!border-transparent dark:!border-transparent ' +
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' +
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
}
>
<CustomIcon <CustomIcon
name="caretDown" name="caretDown"
className={ className={
@ -47,14 +37,6 @@ export function ActionButtonDropdown({
<span className="sr-only"> <span className="sr-only">
{props.name ? props.name + ': ' : ''}open menu {props.name ? props.name + ': ' : ''}open menu
</span> </span>
<Tooltip
delay={0}
position="bottom"
hoverOnly
wrapperClassName="ui-open:!hidden"
>
{dropdownTooltipText}
</Tooltip>
</Popover.Button> </Popover.Button>
<Popover.Panel <Popover.Panel
as="ul" as="ul"

View File

@ -129,13 +129,11 @@ function CommandArgOptionInput({
<label <label
htmlFor="option-input" htmlFor="option-input"
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80" className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
data-testid="cmd-bar-arg-name"
> >
{argName} {argName}
</label> </label>
<Combobox.Input <Combobox.Input
id="option-input" id="option-input"
data-testid="cmd-bar-arg-value"
ref={inputRef} ref={inputRef}
onChange={(event) => onChange={(event) =>
!event.target.disabled && setQuery(event.target.value) !event.target.disabled && setQuery(event.target.value)

View File

@ -52,7 +52,6 @@ function CommandComboBox({
className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit" className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit"
/> />
<Combobox.Input <Combobox.Input
data-testid="cmd-bar-search"
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none" className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
onKeyDown={(event) => { onKeyDown={(event) => {
@ -86,7 +85,6 @@ function CommandComboBox({
value={option} value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50" className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
disabled={optionIsDisabled(option)} disabled={optionIsDisabled(option)}
data-testid={`cmd-bar-option`}
> >
{'icon' in option && option.icon && ( {'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" /> <CustomIcon name={option.icon} className="w-5 h-5" />

View File

@ -48,9 +48,8 @@ export const FileMachineProvider = ({
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { settings, auth } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>( const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
[] []
) )
@ -297,14 +296,8 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo( const kclCommandMemo = useMemo(
() => () =>
kclCommands({ kclCommands(
authToken: auth?.context?.token ?? '', async (data) => {
projectData,
settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
},
specialPropsForSampleCommand: {
onSubmit: async (data) => {
if (data.method === 'overwrite') { if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code) codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true) await kclManager.executeCode(true)
@ -332,12 +325,11 @@ export const FileMachineProvider = ({
}) })
} }
}, },
providedOptions: kclSamples.map((sample) => ({ kclSamples.map((sample) => ({
value: sample.pathFromProjectDirectoryToFirstFile, value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title, name: sample.title,
})), }))
}, ).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]

View File

@ -95,11 +95,9 @@ export const processMemory = (programMemory: ProgramMemory) => {
) { ) {
const sk = sketchFromKclValueOptional(val, key) const sk = sketchFromKclValueOptional(val, key)
if (val.type === 'Solid') { if (val.type === 'Solid') {
processedMemory[key] = val.value.value.map( processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
({ ...rest }: ExtrudeSurface) => {
return rest return rest
} })
)
} else if (!(sk instanceof Reason)) { } else if (!(sk instanceof Reason)) {
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => { processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest return rest

View File

@ -1,68 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { OpenInDesktopAppHandler } from './OpenInDesktopAppHandler'
/**
* The behavior under test requires a router,
* so we wrap the component in a minimal router setup.
*/
function TestingMinimalRouterWrapper({
children,
location,
}: {
location?: string
children: React.ReactNode
}) {
return (
<Routes location={location}>
<Route
path="/"
element={<OpenInDesktopAppHandler>{children}</OpenInDesktopAppHandler>}
/>
</Routes>
)
}
describe('OpenInDesktopAppHandler tests', () => {
test(`does not render the modal if no query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper>
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
const dummyAppContents = screen.getByText('Dummy app contents')
const modalContents = screen.queryByText('Open in desktop app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalContents).not.toBeInTheDocument()
})
test(`renders the modal if the query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper location="/?ask-open-desktop">
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
let dummyAppContents = screen.queryByText('Dummy app contents')
let modalButton = screen.queryByText('Continue to web app')
// Starts as disconnected
expect(dummyAppContents).not.toBeInTheDocument()
expect(modalButton).not.toBeFalsy()
expect(modalButton).toBeInTheDocument()
fireEvent.click(modalButton as Element)
// I don't like that you have to re-query the screen here
dummyAppContents = screen.queryByText('Dummy app contents')
modalButton = screen.queryByText('Continue to web app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalButton).not.toBeInTheDocument()
})
})

View File

@ -1,125 +0,0 @@
import { getSystemTheme, Themes } from 'lib/theme'
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
import { isDesktop } from 'lib/isDesktop'
import { useSearchParams } from 'react-router-dom'
import { ASK_TO_OPEN_QUERY_PARAM } from 'lib/constants'
import { VITE_KC_SITE_BASE_URL } from 'env'
import { ActionButton } from './ActionButton'
import { Transition } from '@headlessui/react'
/**
* This component is a handler that checks if a certain query parameter
* is present, and if so, it will show a modal asking the user if they
* want to open the current page in the desktop app.
*/
export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
const theme = getSystemTheme()
const buttonClasses =
'bg-transparent flex-0 hover:bg-primary/10 dark:hover:bg-primary/10'
const pathLogomarkSvg = `${isDesktop() ? '.' : ''}/zma-logomark${
theme === Themes.Light ? '-dark' : ''
}.svg`
const [searchParams, setSearchParams] = useSearchParams()
// We also ignore this param on desktop, as it is redundant
const hasAskToOpenParam =
!isDesktop() && searchParams.has(ASK_TO_OPEN_QUERY_PARAM)
/**
* This function removes the query param to ask to open in desktop app
* and then navigates to the same route but with our custom protocol
* `zoo-studio:` instead of `https://${BASE_URL}`, to trigger the user's
* desktop app to open.
*/
function onOpenInDesktopApp() {
const newSearchParams = new URLSearchParams(globalThis.location.search)
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
const newURL = `${ZOO_STUDIO_PROTOCOL}${globalThis.location.pathname.replace(
'/',
''
)}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`
globalThis.location.href = newURL
}
/**
* Just remove the query param to ask to open in desktop app
* and continue to the web app.
*/
function continueToWebApp() {
searchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
setSearchParams(searchParams)
}
return hasAskToOpenParam ? (
<Transition
appear
show={true}
as="div"
className={
theme +
` fixed inset-0 grid p-4 place-content-center ${
theme === Themes.Dark ? '!bg-chalkboard-110 text-chalkboard-20' : ''
}`
}
>
<Transition.Child
as="div"
className={`max-w-3xl py-6 px-10 flex flex-col items-center gap-8
mx-auto border rounded-lg shadow-lg dark:bg-chalkboard-100`}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
style={{ zIndex: 10 }}
>
<div>
<h1 className="text-2xl">
Launching{' '}
<img
src={pathLogomarkSvg}
className="w-48"
alt="Zoo Modeling App"
/>
</h1>
</div>
<p className="text-primary flex items-center gap-2">
Choose where to open this link...
</p>
<div className="flex flex-col md:flex-row items-start justify-between gap-4 xl:gap-8">
<div className="flex flex-col gap-2">
<ActionButton
Element="button"
className={buttonClasses + ' !text-base'}
onClick={onOpenInDesktopApp}
iconEnd={{ icon: 'arrowRight' }}
>
Open in desktop app
</ActionButton>
<ActionButton
Element="externalLink"
className={
buttonClasses +
' text-sm border-transparent justify-center dark:bg-transparent'
}
to={`${VITE_KC_SITE_BASE_URL}/modeling-app/download`}
iconEnd={{ icon: 'link', bgClassName: '!bg-transparent' }}
>
Download desktop app
</ActionButton>
</div>
<ActionButton
Element="button"
className={buttonClasses + ' -order-1 !text-base'}
onClick={continueToWebApp}
iconStart={{ icon: 'arrowLeft' }}
>
Continue to web app
</ActionButton>
</div>
</Transition.Child>
</Transition>
) : (
props.children
)
}

View File

@ -10,13 +10,11 @@ import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -97,7 +95,6 @@ function ProjectMenuPopover({
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { settings, auth } = useSettingsAuthContext()
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
@ -158,6 +155,7 @@ function ProjectMenuPopover({
data: exportCommandInfo, data: exportCommandInfo,
}), }),
}, },
'break',
{ {
id: 'make', id: 'make',
Element: 'button', Element: 'button',
@ -183,19 +181,6 @@ function ProjectMenuPopover({
}) })
}, },
}, },
{
id: 'share-link',
Element: 'button',
children: 'Share link to file',
onClick: async () => {
await copyFileShareLink({
token: auth?.context.token || '',
code: codeManager.code,
name: project?.name || '',
units: settings.context.modeling.defaultUnit.current,
})
},
},
'break', 'break',
{ {
id: 'go-home', id: 'go-home',

View File

@ -3,11 +3,11 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader' import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { projectsMachine } from 'machines/projectsMachine' import { projectsMachine } from 'machines/projectsMachine'
import { createContext, useCallback, useEffect, useState } from 'react' import { createContext, useEffect, useState } from 'react'
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate' import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
createNewProjectDirectory, createNewProjectDirectory,
@ -18,28 +18,11 @@ import {
getNextProjectIndex, getNextProjectIndex,
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated, doesProjectNameNeedInterpolated,
getUniqueProjectName,
getNextFileName,
} from 'lib/desktopFS' } from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig' import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import {
CREATE_FILE_URL_PARAM,
FILE_EXT,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { codeManager } from 'lib/singletons'
import {
loadAndValidateSettings,
projectConfigurationToSettingsPayload,
saveSettings,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { Project } from 'lib/project'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state?: StateFrom<T> state?: StateFrom<T>
@ -69,110 +52,12 @@ export const ProjectsContextProvider = ({
) )
} }
/**
* We need some of the functionality of the ProjectsContextProvider in the web version
* but we can't perform file system operations in the browser,
* so most of the behavior of this machine is stubbed out.
*/
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => { const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
const [searchParams, setSearchParams] = useSearchParams()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const {
settings: { context: settings, send: settingsSend },
} = useSettingsAuthContext()
const [state, send, actor] = useMachine(
projectsMachine.provide({
actions: {
navigateToProject: () => {},
navigateToProjectIfNeeded: () => {},
navigateToFile: () => {},
toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
'message' in event.output &&
typeof event.output.message === 'string' &&
event.output.message) ||
''
),
toastError: ({ event }) =>
toast.error(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
typeof event.output === 'string' &&
event.output) ||
''
),
},
actors: {
readProjects: fromPromise(async () => [] as Project[]),
createProject: fromPromise(async () => ({
message: 'not implemented on web',
})),
renameProject: fromPromise(async () => ({
message: 'not implemented on web',
oldName: '',
newName: '',
})),
deleteProject: fromPromise(async () => ({
message: 'not implemented on web',
name: '',
})),
createFile: fromPromise(async ({ input }) => {
// Browser version doesn't navigate, just overwrites the current file
clearImportSearchParams()
codeManager.updateCodeStateEditor(input.code || '')
await codeManager.writeToFile()
settingsSend({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: input.units,
},
})
return {
message: 'File and units overwritten successfully',
fileName: input.name,
projectName: '',
}
}),
},
}),
{
input: {
projects: [],
defaultProjectName: settings.projects.defaultProjectName.current,
defaultDirectory: settings.app.projectDirectory.current,
},
}
)
// register all project-related command palette commands
useStateMachineCommands({
machineId: 'projects',
send,
state,
commandBarConfig: projectsCommandBarConfig,
actor,
onCancel: clearImportSearchParams,
})
return ( return (
<ProjectsMachineContext.Provider <ProjectsMachineContext.Provider
value={{ value={{
state, state: undefined,
send, send: () => {},
}} }}
> >
{children} {children}
@ -187,22 +72,19 @@ const ProjectsContextDesktop = ({
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const [searchParams, setSearchParams] = useSearchParams()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { onProjectOpen } = useLspContext() const { onProjectOpen } = useLspContext()
const { const {
settings: { context: settings }, settings: { context: settings },
} = useSettingsAuthContext() } = useSettingsAuthContext()
useEffect(() => {
console.log(
'project directory changed',
settings.app.projectDirectory.current
)
}, [settings.app.projectDirectory.current])
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0) const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([ const { projectPaths, projectsDir } = useProjectsLoader([
projectsLoaderTrigger, projectsLoaderTrigger,
@ -286,31 +168,6 @@ const ProjectsContextDesktop = ({
} }
} }
}, },
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-file') return
// For now, the browser version of create-file doesn't need to navigate
// since it just overwrites the current file.
if (!isDesktop()) return
let projectPath = window.electron.join(
context.defaultDirectory,
event.output.projectName
)
let filePath = window.electron.join(
projectPath,
event.output.fileName
)
onProjectOpen(
{
name: event.output.projectName,
path: projectPath,
},
null
)
const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
filePath
)}`
navigate(pathToNavigateTo)
},
toastSuccess: ({ event }) => toastSuccess: ({ event }) =>
toast.success( toast.success(
('data' in event && typeof event.data === 'string' && event.data) || ('data' in event && typeof event.data === 'string' && event.data) ||
@ -338,12 +195,16 @@ const ProjectsContextDesktop = ({
: settings.projects.defaultProjectName.current : settings.projects.defaultProjectName.current
).trim() ).trim()
const uniqueName = getUniqueProjectName(name, input.projects) if (doesProjectNameNeedInterpolated(name)) {
await createNewProjectDirectory(uniqueName) const nextIndex = getNextProjectIndex(name, input.projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
return { return {
message: `Successfully created "${uniqueName}"`, message: `Successfully created "${name}"`,
name: uniqueName, name,
} }
}), }),
renameProject: fromPromise(async ({ input }) => { renameProject: fromPromise(async ({ input }) => {
@ -360,6 +221,8 @@ const ProjectsContextDesktop = ({
name = interpolateProjectNameWithIndex(name, nextIndex) name = interpolateProjectNameWithIndex(name, nextIndex)
} }
console.log('from Project')
await renameProjectDirectory( await renameProjectDirectory(
window.electron.path.join(defaultDirectory, oldName), window.electron.path.join(defaultDirectory, oldName),
name name
@ -382,83 +245,14 @@ const ProjectsContextDesktop = ({
name: input.name, name: input.name,
} }
}), }),
createFile: fromPromise(async ({ input }) => {
let projectName =
(input.method === 'newProject' ? input.name : input.projectName) ||
settings.projects.defaultProjectName.current
let fileName =
input.method === 'newProject'
? PROJECT_ENTRYPOINT
: input.name.endsWith(FILE_EXT)
? input.name
: input.name + FILE_EXT
let message = 'File created successfully'
const unitsConfiguration: DeepPartial<Configuration> = {
settings: {
project: {
directory: settings.app.projectDirectory.current,
}, },
modeling: { guards: {
base_unit: input.units, 'Has at least 1 project': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-projects') return false
console.log(`from has at least 1 project: ${event.output.length}`)
return event.output.length ? event.output.length >= 1 : false
}, },
}, },
}
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
if (needsInterpolated) {
const nextIndex = getNextProjectIndex(projectName, input.projects)
projectName = interpolateProjectNameWithIndex(
projectName,
nextIndex
)
}
// Create the project around the file if newProject
if (input.method === 'newProject') {
await createNewProjectDirectory(
projectName,
input.code,
unitsConfiguration
)
message = `Project "${projectName}" created successfully with link contents`
} else {
let projectPath = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
message = `File "${fileName}" created successfully`
const existingConfiguration = await loadAndValidateSettings(
projectPath
)
const settingsToSave = setSettingsAtLevel(
existingConfiguration.settings,
'project',
projectConfigurationToSettingsPayload(unitsConfiguration)
)
await saveSettings(settingsToSave, projectPath)
}
// Create the file
let baseDir = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
const { name, path } = getNextFileName({
entryName: fileName,
baseDir,
})
fileName = name
await window.electron.writeFile(path, input.code || '')
return {
message,
fileName,
projectName,
}
}),
},
}), }),
{ {
input: { input: {
@ -480,7 +274,6 @@ const ProjectsContextDesktop = ({
state, state,
commandBarConfig: projectsCommandBarConfig, commandBarConfig: projectsCommandBarConfig,
actor, actor,
onCancel: clearImportSearchParams,
}) })
return ( return (

View File

@ -6,6 +6,5 @@ export const useCommandsContext = () => {
return { return {
commandBarSend: commandBarActor.send, commandBarSend: commandBarActor.send,
commandBarState, commandBarState,
commandBarActor,
} }
} }

View File

@ -1,65 +0,0 @@
import { base64ToString } from 'lib/base64'
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
import { useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useSettingsAuthContext } from './useSettingsAuthContext'
import { isDesktop } from 'lib/isDesktop'
import { FileLinkParams } from 'lib/links'
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
// For initializing the command arguments, we actually want `method` to be undefined
// so that we don't skip it in the command palette.
export type CreateFileSchemaMethodOptional = Omit<
ProjectsCommandSchema['Import file from URL'],
'method'
> & {
method?: 'newProject' | 'existingProject'
}
/**
* companion to createFileLink. This hook runs an effect on mount that
* checks the URL for the CREATE_FILE_URL_PARAM and triggers the "Create file"
* command if it is present, loading the command's default values from the other
* URL parameters.
*/
export function useCreateFileLinkQuery(
callback: (args: CreateFileSchemaMethodOptional) => void
) {
const [searchParams] = useSearchParams()
const { settings } = useSettingsAuthContext()
useEffect(() => {
const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
if (createFileParam) {
const params: FileLinkParams = {
code: base64ToString(
decodeURIComponent(searchParams.get('code') ?? '')
),
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
units:
(baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
settings.context.modeling.defaultUnit.default) ??
settings.context.modeling.defaultUnit.current,
}
const argDefaultValues: CreateFileSchemaMethodOptional = {
name: params.name
? isDesktop()
? params.name.replace('.kcl', '')
: params.name
: isDesktop()
? settings.context.projects.defaultProjectName.current
: DEFAULT_FILE_NAME,
code: params.code || '',
units: params.units,
method: isDesktop() ? undefined : 'existingProject',
}
callback(argDefaultValues)
}
}, [searchParams])
}

View File

@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
useEffect(() => { useEffect(() => {
// Useless on web, until we get fake filesystems over there. // Useless on web, until we get fake filesystems over there.
if (!isDesktop()) return if (!isDesktop) return
if (deps && deps[0] === lastTs) return if (deps && deps[0] === lastTs) return

View File

@ -24,10 +24,7 @@ describe('testing AST', () => {
type: 'Literal', type: 'Literal',
start: 0, start: 0,
end: 1, end: 1,
value: {
suffix: 'None',
value: 5, value: 5,
},
raw: '5', raw: '5',
}, },
operator: '+', operator: '+',
@ -35,10 +32,7 @@ describe('testing AST', () => {
type: 'Literal', type: 'Literal',
start: 3, start: 3,
end: 4, end: 4,
value: {
suffix: 'None',
value: 6, value: 6,
},
raw: '6', raw: '6',
}, },
}, },

View File

@ -54,9 +54,6 @@ const mySketch001 = startSketchOn('XY')
}, },
], ],
id: expect.any(String), id: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }], __meta: [{ sourceRange: [46, 71, 0] }],
}, },
}) })
@ -74,8 +71,6 @@ const mySketch001 = startSketchOn('XY')
// @ts-ignore // @ts-ignore
const sketch001 = execState.memory.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'Solid',
value: {
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
value: [ value: [
@ -96,9 +91,6 @@ const mySketch001 = startSketchOn('XY')
], ],
sketch: { sketch: {
id: expect.any(String), id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array), __meta: expect.any(Array),
on: expect.any(Object), on: expect.any(Object),
start: expect.any(Object), start: expect.any(Object),
@ -129,11 +121,7 @@ const mySketch001 = startSketchOn('XY')
height: 2, height: 2,
startCapId: expect.any(String), startCapId: expect.any(String),
endCapId: expect.any(String), endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }], __meta: [{ sourceRange: [46, 71, 0] }],
},
}) })
}) })
test('sketch extrude and sketch on one of the faces', async () => { test('sketch extrude and sketch on one of the faces', async () => {
@ -165,8 +153,6 @@ const sk2 = startSketchOn('XY')
const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')] const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'Solid',
value: {
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
value: [ value: [
@ -203,9 +189,6 @@ const sk2 = startSketchOn('XY')
on: expect.any(Object), on: expect.any(Object),
start: expect.any(Object), start: expect.any(Object),
type: 'Sketch', type: 'Sketch',
units: {
type: 'Mm',
},
tags: { tags: {
p: { p: {
__meta: [ __meta: [
@ -259,15 +242,9 @@ const sk2 = startSketchOn('XY')
height: 2, height: 2,
startCapId: expect.any(String), startCapId: expect.any(String),
endCapId: expect.any(String), endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [38, 63, 0] }], __meta: [{ sourceRange: [38, 63, 0] }],
}, },
},
{ {
type: 'Solid',
value: {
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
value: [ value: [
@ -300,9 +277,6 @@ const sk2 = startSketchOn('XY')
], ],
sketch: { sketch: {
id: expect.any(String), id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array), __meta: expect.any(Array),
on: expect.any(Object), on: expect.any(Object),
start: expect.any(Object), start: expect.any(Object),
@ -361,10 +335,6 @@ const sk2 = startSketchOn('XY')
startCapId: expect.any(String), startCapId: expect.any(String),
endCapId: expect.any(String), endCapId: expect.any(String),
__meta: [{ sourceRange: [342, 367, 0] }], __meta: [{ sourceRange: [342, 367, 0] }],
units: {
type: 'Mm',
},
},
}, },
]) ])
}) })

View File

@ -221,9 +221,6 @@ const newVar = myVar + 1`
}, },
], ],
id: expect.any(String), id: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [39, 63, 0] }], __meta: [{ sourceRange: [39, 63, 0] }],
}, },
}) })

View File

@ -39,7 +39,7 @@ describe('Testing createLiteral', () => {
it('should create a literal', () => { it('should create a literal', () => {
const result = createLiteral(5) const result = createLiteral(5)
expect(result.type).toBe('Literal') expect(result.type).toBe('Literal')
expect((result as any).value.value).toBe(5) expect(result.value).toBe(5)
}) })
}) })
describe('Testing createIdentifier', () => { describe('Testing createIdentifier', () => {
@ -56,7 +56,7 @@ describe('Testing createCallExpression', () => {
expect(result.callee.type).toBe('Identifier') expect(result.callee.type).toBe('Identifier')
expect(result.callee.name).toBe('myFunc') expect(result.callee.name).toBe('myFunc')
expect(result.arguments[0].type).toBe('Literal') expect(result.arguments[0].type).toBe('Literal')
expect((result.arguments[0] as any).value.value).toBe(5) expect((result.arguments[0] as any).value).toBe(5)
}) })
}) })
describe('Testing createObjectExpression', () => { describe('Testing createObjectExpression', () => {
@ -68,7 +68,7 @@ describe('Testing createObjectExpression', () => {
expect(result.properties[0].type).toBe('ObjectProperty') expect(result.properties[0].type).toBe('ObjectProperty')
expect(result.properties[0].key.name).toBe('myProp') expect(result.properties[0].key.name).toBe('myProp')
expect(result.properties[0].value.type).toBe('Literal') expect(result.properties[0].value.type).toBe('Literal')
expect((result.properties[0].value as any).value.value).toBe(5) expect((result.properties[0].value as any).value).toBe(5)
}) })
}) })
describe('Testing createArrayExpression', () => { describe('Testing createArrayExpression', () => {
@ -76,7 +76,7 @@ describe('Testing createArrayExpression', () => {
const result = createArrayExpression([createLiteral(5)]) const result = createArrayExpression([createLiteral(5)])
expect(result.type).toBe('ArrayExpression') expect(result.type).toBe('ArrayExpression')
expect(result.elements[0].type).toBe('Literal') expect(result.elements[0].type).toBe('Literal')
expect((result.elements[0] as any).value.value).toBe(5) expect((result.elements[0] as any).value).toBe(5)
}) })
}) })
describe('Testing createPipeSubstitution', () => { describe('Testing createPipeSubstitution', () => {
@ -93,7 +93,7 @@ describe('Testing createVariableDeclaration', () => {
expect(result.declaration.id.type).toBe('Identifier') expect(result.declaration.id.type).toBe('Identifier')
expect(result.declaration.id.name).toBe('myVar') expect(result.declaration.id.name).toBe('myVar')
expect(result.declaration.init.type).toBe('Literal') expect(result.declaration.init.type).toBe('Literal')
expect((result.declaration.init as any).value.value).toBe(5) expect((result.declaration.init as any).value).toBe(5)
}) })
}) })
describe('Testing createPipeExpression', () => { describe('Testing createPipeExpression', () => {
@ -101,7 +101,7 @@ describe('Testing createPipeExpression', () => {
const result = createPipeExpression([createLiteral(5)]) const result = createPipeExpression([createLiteral(5)])
expect(result.type).toBe('PipeExpression') expect(result.type).toBe('PipeExpression')
expect(result.body[0].type).toBe('Literal') expect(result.body[0].type).toBe('Literal')
expect((result.body[0] as any).value.value).toBe(5) expect((result.body[0] as any).value).toBe(5)
}) })
}) })

View File

@ -743,18 +743,14 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1)) return splitPathAtPipeExpression(pathToNode.slice(0, -1))
} }
export function createLiteral(value: LiteralValue | number): Node<Literal> { export function createLiteral(value: LiteralValue): Node<Literal> {
const raw = `${value}`
if (typeof value === 'number') {
value = { value, suffix: 'None' }
}
return { return {
type: 'Literal', type: 'Literal',
start: 0, start: 0,
end: 0, end: 0,
moduleId: 0, moduleId: 0,
value, value,
raw, raw: `${value}`,
} }
} }

View File

@ -660,7 +660,7 @@ myNestedVar = [
enter: (node, path) => { enter: (node, path) => {
if ( if (
node.type === 'Literal' && node.type === 'Literal' &&
String((node as any).value.value) === literalOfInterest String(node.value) === literalOfInterest
) { ) {
pathToNode = path pathToNode = path
} else if ( } else if (

View File

@ -717,6 +717,16 @@ function isTypeInArrayExp(
return node.elements.some((el) => isTypeInValue(el, syntaxType)) return node.elements.some((el) => isTypeInValue(el, syntaxType))
} }
export function isValueZero(val?: Expr): boolean {
return (
(val?.type === 'Literal' && Number(val.value) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
}
export function isLinesParallelAndConstrained( export function isLinesParallelAndConstrained(
ast: Program, ast: Program,
artifactGraph: ArtifactGraph, artifactGraph: ArtifactGraph,

View File

@ -1014,11 +1014,6 @@ class EngineConnection extends EventTarget {
this.pingPongSpan.pong = new Date() this.pingPongSpan.pong = new Date()
break break
case 'modeling_session_data':
let api_call_id = resp.data?.session?.api_call_id
console.log(`API Call ID: ${api_call_id}`)
break
// Only fires on successful authentication. // Only fires on successful authentication.
case 'ice_server_info': case 'ice_server_info':
let ice_servers = resp.data?.ice_servers let ice_servers = resp.data?.ice_servers

View File

@ -20,12 +20,12 @@ import {
sketchFromKclValue, sketchFromKclValue,
Literal, Literal,
SourceRange, SourceRange,
LiteralValue,
} from '../wasm' } from '../wasm'
import { import {
getNodeFromPath, getNodeFromPath,
getNodeFromPathCurry, getNodeFromPathCurry,
getNodePathFromSourceRange, getNodePathFromSourceRange,
isValueZero,
} from '../queryAst' } from '../queryAst'
import { import {
createArrayExpression, createArrayExpression,
@ -79,32 +79,11 @@ export type ConstraintType =
| 'setAngleBetween' | 'setAngleBetween'
const REF_NUM_ERR = new Error('Referenced segment does not have a to value') const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
function asNum(val: LiteralValue): number | Error {
if (typeof val === 'object') return val.value
return REF_NUM_ERR
}
function forceNum(arg: Literal): number {
if (typeof arg.value === 'boolean' || typeof arg.value === 'string') {
return Number(arg.value)
} else {
return arg.value.value
}
}
function isUndef(val: any): val is undefined { function isUndef(val: any): val is undefined {
return typeof val === 'undefined' return typeof val === 'undefined'
} }
function isNum(val: any): val is number {
function isValueZero(val?: Expr): boolean { return typeof val === 'number'
return (
(val?.type === 'Literal' && forceNum(val) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
} }
function createCallWrapper( function createCallWrapper(
@ -211,7 +190,7 @@ const xyLineSetLength =
: referenceSeg : referenceSeg
? segRef ? segRef
: args[0].expr : args[0].expr
const literalARg = asNum(args[0].expr.value) const literalARg = getArgLiteralVal(args[0].expr)
if (err(literalARg)) return literalARg if (err(literalARg)) return literalARg
return createCallWrapper(xOrY, lineVal, tag, literalARg) return createCallWrapper(xOrY, lineVal, tag, literalARg)
} }
@ -232,14 +211,13 @@ const basicAngledLineCreateNode =
referencedSegment: path, referencedSegment: path,
}) => { }) => {
const refAng = path ? getAngle(path?.from, path?.to) : 0 const refAng = path ? getAngle(path?.from, path?.to) : 0
const argValue = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(argValue)) return argValue
const nonForcedAng = const nonForcedAng =
varValToUse === 'ang' varValToUse === 'ang'
? inputs[0].expr ? inputs[0].expr
: referenceSeg === 'ang' : referenceSeg === 'ang'
? getClosesAngleDirection( ? getClosesAngleDirection(
argValue, args[0].expr.value,
refAng, refAng,
createSegAngle(referenceSegName) createSegAngle(referenceSegName)
) )
@ -252,8 +230,8 @@ const basicAngledLineCreateNode =
: args[1].expr : args[1].expr
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
const literalArg = asNum( const literalArg = getArgLiteralVal(
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value valToForce === 'ang' ? args[0].expr : args[1].expr
) )
if (err(literalArg)) return literalArg if (err(literalArg)) return literalArg
return createCallWrapper( return createCallWrapper(
@ -305,7 +283,7 @@ const getMinAndSegAngVals = (
} }
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) => const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal Number(arg.value) < 0 ? createUnaryExpression(legLenVal) : legLenVal
const getLegAng = (ang: number, legAngleVal: BinaryPart) => { const getLegAng = (ang: number, legAngleVal: BinaryPart) => {
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360 const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
@ -344,7 +322,8 @@ const setHorzVertDistanceCreateNode =
referencedSegment, referencedSegment,
}) => { }) => {
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[index].expr.value) const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return literalArg
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2) const valueUsedInTransform = roundOff(literalArg - refNum, 2)
@ -373,7 +352,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
referencedSegment, referencedSegment,
}) => { }) => {
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[1].expr.value) const literalArg = getArgLiteralVal(args?.[1].expr)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2) const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const binExp = createBinaryExpressionWithUnary([ const binExp = createBinaryExpressionWithUnary([
@ -395,8 +374,8 @@ const setAbsDistanceCreateNode =
index = xOrY === 'x' ? 0 : 1 index = xOrY === 'x' ? 0 : 1
): CreateStdLibSketchCallExpr => ): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, rawArgs: args }) => { ({ tag, forceValueUsedInTransform, rawArgs: args }) => {
const literalArg = asNum(args?.[index].expr.value) const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return literalArg if (err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg, 2) const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform) const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
if (isXOrYLine) { if (isXOrYLine) {
@ -417,8 +396,8 @@ const setAbsDistanceCreateNode =
const setAbsDistanceForAngleLineCreateNode = const setAbsDistanceForAngleLineCreateNode =
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr => (xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => { ({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
const literalArg = asNum(args?.[1].expr.value) const literalArg = getArgLiteralVal(args?.[1].expr)
if (err(literalArg)) return literalArg if (err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg, 2) const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform) const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
return createCallWrapper( return createCallWrapper(
@ -440,7 +419,7 @@ const setHorVertDistanceForXYLines =
}) => { }) => {
const index = xOrY === 'x' ? 0 : 1 const index = xOrY === 'x' ? 0 : 1
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[index].expr.value) const literalArg = getArgLiteralVal(args?.[index].expr)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2) const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const makeBinExp = createBinaryExpressionWithUnary([ const makeBinExp = createBinaryExpressionWithUnary([
@ -466,9 +445,9 @@ const setHorzVertDistanceConstraintLineCreateNode =
]) ])
const makeBinExp = (index: 0 | 1) => { const makeBinExp = (index: 0 | 1) => {
const arg = asNum(args?.[index].expr.value) const arg = getArgLiteralVal(args?.[index].expr)
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
if (err(arg) || isUndef(refNum)) return REF_NUM_ERR if (err(arg) || !isNum(refNum)) return REF_NUM_ERR
return createBinaryExpressionWithUnary([ return createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, isX), createSegEnd(referenceSegName, isX),
createLiteral(roundOff(arg - refNum, 2)), createLiteral(roundOff(arg - refNum, 2)),
@ -489,9 +468,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
forceValueUsedInTransform, forceValueUsedInTransform,
rawArgs: args, rawArgs: args,
}) => { }) => {
const val = asNum(args[1].expr.value), const val = args[1].expr.value,
angle = asNum(args[0].expr.value) angle = args[0].expr.value
if (err(val) || err(angle)) return REF_NUM_ERR if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(val, 2) const valueUsedInTransform = roundOff(val, 2)
const varNamMap: { [key: number]: string } = { const varNamMap: { [key: number]: string } = {
0: 'ZERO', 0: 'ZERO',
@ -519,8 +498,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
inputs, inputs,
rawArgs: args, rawArgs: args,
}) => { }) => {
const val = asNum(args[1].expr.value) const val = args[1].expr.value
if (err(val)) return val if (!isNum(val)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(val, 2) const valueUsedInTransform = roundOff(val, 2)
return intersectCallWrapper({ return intersectCallWrapper({
fnName: 'angledLineThatIntersects', fnName: 'angledLineThatIntersects',
@ -545,8 +524,8 @@ const setAngleBetweenCreateNode =
const refAngle = referencedSegment const refAngle = referencedSegment
? getAngle(referencedSegment?.from, referencedSegment?.to) ? getAngle(referencedSegment?.from, referencedSegment?.to)
: 0 : 0
const val = asNum(args[0].expr.value) const val = args[0].expr.value
if (err(val)) return val if (!isNum(val)) return REF_NUM_ERR
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle)) let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
let firstHalfValue = createSegAngle(referenceSegName) let firstHalfValue = createSegAngle(referenceSegName)
if (Math.abs(valueUsedInTransform) > 90) { if (Math.abs(valueUsedInTransform) > 90) {
@ -727,11 +706,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToX', 'angledLineToX',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[0].expr,
],
tag tag
) )
}, },
@ -758,11 +739,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToY', 'angledLineToY',
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall),
inputs[1].expr,
],
tag tag
) )
}, },
@ -780,7 +763,7 @@ const transformMap: TransformMap = {
forceValueUsedInTransform, forceValueUsedInTransform,
rawArgs: args, rawArgs: args,
}) => { }) => {
const val = asNum(args[0].expr.value) const val = getArgLiteralVal(args[0].expr)
if (err(val)) return val if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToY', 'angledLineToY',
@ -861,7 +844,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine', tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('yLine', expr, tag) return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag) return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -873,7 +856,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine', tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('xLine', expr, tag) return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag) return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -917,11 +900,10 @@ const transformMap: TransformMap = {
referenceSegName, referenceSegName,
getInputOfType(inputs, 'xRelative').expr getInputOfType(inputs, 'xRelative').expr
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineOfXLength', 'angledLineOfXLength',
[getLegAng(val, legAngle), minVal], [getLegAng(args[0].expr.value, legAngle), minVal],
tag tag
) )
}, },
@ -930,7 +912,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine', tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('xLine', expr, tag) return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag) return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -971,11 +953,10 @@ const transformMap: TransformMap = {
inputs[1].expr, inputs[1].expr,
'legAngY' 'legAngY'
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineOfXLength', 'angledLineOfXLength',
[getLegAng(val, legAngle), minVal], [getLegAng(args[0].expr.value, legAngle), minVal],
tag tag
) )
}, },
@ -984,7 +965,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine', tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('yLine', expr, tag) return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag) return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -1024,11 +1005,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToX', 'angledLineToX',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
tag tag
) )
}, },
@ -1074,11 +1057,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToY', 'angledLineToY',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
tag tag
) )
}, },
@ -1095,7 +1080,7 @@ const transformMap: TransformMap = {
equalLength: { equalLength: {
tooltip: 'xLine', tooltip: 'xLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => { createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = asNum(args[0].expr.value) const argVal = getArgLiteralVal(args[0].expr)
if (err(argVal)) return argVal if (err(argVal)) return argVal
const segLen = createSegLen(referenceSegName) const segLen = createSegLen(referenceSegName)
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal) if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
@ -1133,7 +1118,7 @@ const transformMap: TransformMap = {
equalLength: { equalLength: {
tooltip: 'yLine', tooltip: 'yLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => { createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = asNum(args[0].expr.value) const argVal = getArgLiteralVal(args[0].expr)
if (err(argVal)) return argVal if (err(argVal)) return argVal
let segLen = createSegLen(referenceSegName) let segLen = createSegLen(referenceSegName)
if (argVal < 0) segLen = createUnaryExpression(segLen) if (argVal < 0) segLen = createUnaryExpression(segLen)
@ -1729,7 +1714,7 @@ export function transformAstSketchLines({
let kclVal = programMemory.get(varName) let kclVal = programMemory.get(varName)
let sketch let sketch
if (kclVal?.type === 'Solid') { if (kclVal?.type === 'Solid') {
sketch = kclVal.value.sketch sketch = kclVal.sketch
} else { } else {
sketch = sketchFromKclValue(kclVal, varName) sketch = sketchFromKclValue(kclVal, varName)
if (err(sketch)) { if (err(sketch)) {
@ -1838,6 +1823,11 @@ function createLastSeg(isX: boolean): Node<CallExpression> {
]) ])
} }
function getArgLiteralVal(arg: Literal): number | Error {
if (!isNum(arg.value)) return REF_NUM_ERR
return arg.value
}
export type ConstraintLevel = 'free' | 'partial' | 'full' export type ConstraintLevel = 'free' | 'partial' | 'full'
export function getConstraintLevelFromSourceRange( export function getConstraintLevelFromSourceRange(

View File

@ -539,8 +539,7 @@ export function sketchFromKclValueOptional(
): Sketch | Reason { ): Sketch | Reason {
if (obj?.value?.type === 'Sketch') return obj.value if (obj?.value?.type === 'Sketch') return obj.value
if (obj?.value?.type === 'Solid') return obj.value.sketch if (obj?.value?.type === 'Solid') return obj.value.sketch
if (obj?.type === 'Sketch') return obj.value if (obj?.type === 'Solid') return obj.sketch
if (obj?.type === 'Solid') return obj.value.sketch
if (!varName) { if (!varName) {
varName = 'a KCL value' varName = 'a KCL value'
} }

View File

@ -1,40 +0,0 @@
import { expect } from 'vitest'
import { base64ToString, stringToBase64 } from './base64'
describe('base64 encoding', () => {
test('to base64, simple code', async () => {
const code = `extrusionDistance = 12`
// Generated by online tool
const expectedBase64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
test(`to base64, code with UTF-8 characters`, async () => {
// example adapted from MDN docs: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
const code = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
// Generated by online tool
const expectedBase64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
// The following are simply the reverse of the above tests
test('from base64, simple code', async () => {
const base64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const expectedCode = `extrusionDistance = 12`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
test(`from base64, code with UTF-8 characters`, async () => {
const base64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const expectedCode = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
})

View File

@ -1,29 +0,0 @@
/**
* Converts a string to a base64 string, preserving the UTF-8 encoding
*/
export function stringToBase64(str: string) {
return bytesToBase64(new TextEncoder().encode(str))
}
/**
* Converts a base64 string to a string, preserving the UTF-8 encoding
*/
export function base64ToString(base64: string) {
return new TextDecoder().decode(base64ToBytes(base64))
}
/**
* From the MDN Web Docs
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
*/
function base64ToBytes(base64: string) {
const binString = atob(base64)
return Uint8Array.from(binString, (m) => m.codePointAt(0)!)
}
function bytesToBase64(bytes: Uint8Array) {
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte)
).join('')
return btoa(binString)
}

View File

@ -1,8 +1,5 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning' import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { isDesktop } from 'lib/isDesktop'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { projectsMachine } from 'machines/projectsMachine' import { projectsMachine } from 'machines/projectsMachine'
export type ProjectsCommandSchema = { export type ProjectsCommandSchema = {
@ -20,13 +17,6 @@ export type ProjectsCommandSchema = {
oldName: string oldName: string
newName: string newName: string
} }
'Import file from URL': {
name: string
code?: string
units: UnitLength_type
method: 'newProject' | 'existingProject'
projectName?: string
}
} }
export const projectsCommandBarConfig: StateMachineCommandSetConfig< export const projectsCommandBarConfig: StateMachineCommandSetConfig<
@ -36,7 +26,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Open project': { 'Open project': {
icon: 'arrowRight', icon: 'arrowRight',
description: 'Open a project', description: 'Open a project',
status: isDesktop() ? 'active' : 'inactive',
args: { args: {
name: { name: {
inputType: 'options', inputType: 'options',
@ -53,7 +42,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Create project': { 'Create project': {
icon: 'folderPlus', icon: 'folderPlus',
description: 'Create a project', description: 'Create a project',
status: isDesktop() ? 'active' : 'inactive',
args: { args: {
name: { name: {
inputType: 'string', inputType: 'string',
@ -65,7 +53,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Delete project': { 'Delete project': {
icon: 'close', icon: 'close',
description: 'Delete a project', description: 'Delete a project',
status: isDesktop() ? 'active' : 'inactive',
needsReview: true, needsReview: true,
reviewMessage: ({ argumentsToSubmit }) => reviewMessage: ({ argumentsToSubmit }) =>
CommandBarOverwriteWarning({ CommandBarOverwriteWarning({
@ -88,7 +75,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
icon: 'folder', icon: 'folder',
description: 'Rename a project', description: 'Rename a project',
needsReview: true, needsReview: true,
status: isDesktop() ? 'active' : 'inactive',
args: { args: {
oldName: { oldName: {
inputType: 'options', inputType: 'options',
@ -106,80 +92,4 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
'Import file from URL': {
icon: 'file',
description: 'Create a file',
needsReview: true,
status: 'active',
args: {
method: {
inputType: 'options',
required: true,
skip: true,
options: isDesktop()
? [
{ name: 'New project', value: 'newProject' },
{ name: 'Existing project', value: 'existingProject' },
]
: [{ name: 'Overwrite', value: 'existingProject' }],
valueSummary(value) {
return isDesktop()
? value === 'newProject'
? 'New project'
: 'Existing project'
: 'Overwrite'
},
},
// TODO: We can't get the currently-opened project to auto-populate here because
// it's not available on projectMachine, but lower in fileMachine. Unify these.
projectName: {
inputType: 'options',
required: (commandsContext) =>
isDesktop() &&
commandsContext.argumentsToSubmit.method === 'existingProject',
skip: true,
options: (_, context) =>
context?.projects.map((p) => ({
name: p.name!,
value: p.name!,
})) || [],
},
name: {
inputType: 'string',
required: isDesktop(),
skip: true,
},
code: {
inputType: 'text',
required: true,
skip: true,
valueSummary(value) {
const lineCount = value?.trim().split('\n').length
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
},
},
units: {
inputType: 'options',
required: false,
skip: true,
options: baseUnitsUnion.map((unit) => ({
name: baseUnitLabels[unit],
value: unit,
})),
},
},
reviewMessage(commandBarContext) {
return isDesktop()
? `Will add the contents from URL to a new ${
commandBarContext.argumentsToSubmit.method === 'newProject'
? 'project with file main.kcl'
: `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
} named "${
commandBarContext.argumentsToSubmit.name
}", and set default units to "${
commandBarContext.argumentsToSubmit.units
}".`
: `Will overwrite the contents of the current file with the contents from the URL.`
},
},
} }

View File

@ -69,7 +69,6 @@ export const KCL_DEFAULT_DEGREE = `360`
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings' export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
export const DEFAULT_HOST = 'https://api.zoo.dev' export const DEFAULT_HOST = 'https://api.zoo.dev'
export const PROD_APP_URL = 'https://app.zoo.dev'
export const SETTINGS_FILE_NAME = 'settings.toml' export const SETTINGS_FILE_NAME = 'settings.toml'
export const TOKEN_FILE_NAME = 'token.txt' export const TOKEN_FILE_NAME = 'token.txt'
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml' export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
@ -111,9 +110,6 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
localFallback: '/kcl-samples-manifest-fallback.json', localFallback: '/kcl-samples-manifest-fallback.json',
} as const } as const
/** URL parameter to create a file */
export const CREATE_FILE_URL_PARAM = 'create-file'
/** Toast id for the app auto-updater toast */ /** Toast id for the app auto-updater toast */
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast' export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
@ -143,12 +139,3 @@ export const VIEW_NAMES_SEMANTIC = {
} as const } as const
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */ /** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button' export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
/** Custom URL protocol our desktop registers */
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio:'
/**
* A query parameter that triggers a modal
* to "open in desktop app" when present in the URL
*/
export const ASK_TO_OPEN_QUERY_PARAM = 'ask-open-desktop'

View File

@ -1,58 +0,0 @@
import { getUniqueProjectName } from './desktopFS'
import { FileEntry } from './project'
/** Create a dummy project */
function project(name: string, children?: FileEntry[]): FileEntry {
return {
name,
children: children || [
{ name: 'main.kcl', children: null, path: 'main.kcl' },
],
path: `/projects/${name}`,
}
}
describe(`Getting unique project names`, () => {
it(`should return the same name if no conflicts`, () => {
const projectName = 'new-project'
const projects = [project('existing-project'), project('another-project')]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe(projectName)
})
it(`should return a unique name if there is a conflict`, () => {
const projectName = 'existing-project'
const projects = [project('existing-project'), project('another-project')]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-1')
})
it(`should increment an ending index until a unique one is found`, () => {
const projectName = 'existing-project-1'
const projects = [
project('existing-project'),
project('existing-project-1'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-3')
})
it(`should prefer the formatting of the index identifier if present`, () => {
const projectName = 'existing-project-$nn'
const projects = [
project('existing-project'),
project('existing-project-1'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-03')
})
it(`be able to get an incrementing index regardless of padding zeroes`, () => {
const projectName = 'existing-project-$nn'
const projects = [
project('existing-project'),
project('existing-project-01'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-03')
})
})

View File

@ -54,10 +54,8 @@ export function getNextProjectIndex(
const matches = projects.map((project) => project.name?.match(regex)) const matches = projects.map((project) => project.name?.match(regex))
const indices = matches const indices = matches
.filter(Boolean) .filter(Boolean)
.map((match) => (match !== null ? match[1] : '-1')) .map((match) => match![1])
.map((maybeMatchIndex) => { .map(Number)
return parseInt(maybeMatchIndex || '0', 10)
})
const maxIndex = Math.max(...indices, -1) const maxIndex = Math.max(...indices, -1)
return maxIndex + 1 return maxIndex + 1
} }
@ -85,33 +83,6 @@ export function doesProjectNameNeedInterpolated(projectName: string) {
return projectName.includes(INDEX_IDENTIFIER) return projectName.includes(INDEX_IDENTIFIER)
} }
/**
* Given a target name, which may include our magic index interpolation string,
* and a list of projects, return a unique name that doesn't conflict with any
* of the existing projects, incrementing any ending number if necessary.
* @param name
* @param projects
* @returns
*/
export function getUniqueProjectName(name: string, projects: FileEntry[]) {
// The name may have our magic index interpolation string in it
const needsInterpolation = doesProjectNameNeedInterpolated(name)
if (needsInterpolation) {
const nextIndex = getNextProjectIndex(name, projects)
return interpolateProjectNameWithIndex(name, nextIndex)
} else {
let newName = name
while (projects.some((project) => project.name === newName)) {
const nameEndsWithNumber = newName.match(/\d+$/)
newName = nameEndsWithNumber
? newName.replace(/\d+$/, (num) => `${parseInt(num, 10) + 1}`)
: `${name}-1`
}
return newName
}
}
function escapeRegExpChars(string: string) { function escapeRegExpChars(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
} }

View File

@ -1,14 +1,12 @@
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning' import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { Command, CommandArgumentOption } from './commandTypes' import { Command, CommandArgumentOption } from './commandTypes'
import { codeManager, kclManager } from './singletons' import { kclManager } from './singletons'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants' import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm' import { parseProjectSettings } from 'lang/wasm'
import { err, reportRejection } from './trap' import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils' import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
import { copyFileShareLink } from './links'
import { IndexLoaderData } from './types'
interface OnSubmitProps { interface OnSubmitProps {
sampleName: string sampleName: string
@ -17,21 +15,10 @@ interface OnSubmitProps {
method: 'overwrite' | 'newFile' method: 'overwrite' | 'newFile'
} }
interface KclCommandConfig { export function kclCommands(
// TODO: find a different approach that doesn't require onSubmit: (p: OnSubmitProps) => Promise<void>,
// special props for a single command
specialPropsForSampleCommand: {
onSubmit: (p: OnSubmitProps) => Promise<void>
providedOptions: CommandArgumentOption<string>[] providedOptions: CommandArgumentOption<string>[]
} ): Command[] {
projectData: IndexLoaderData
authToken: string
settings: {
defaultUnit: UnitLength_type
}
}
export function kclCommands(commandProps: KclCommandConfig): Command[] {
return [ return [
{ {
name: 'format-code', name: 'format-code',
@ -120,9 +107,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
) )
.then((props) => { .then((props) => {
if (props?.code) { if (props?.code) {
commandProps.specialPropsForSampleCommand onSubmit(props).catch(reportError)
.onSubmit(props)
.catch(reportError)
} }
}) })
.catch(reportError) .catch(reportError)
@ -164,25 +149,9 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
} }
return value return value
}, },
options: commandProps.specialPropsForSampleCommand.providedOptions, options: providedOptions,
}, },
}, },
}, },
{
name: 'share-file-link',
displayName: 'Share file',
description: 'Create a link that contains a copy of the current file.',
groupId: 'code',
needsReview: false,
icon: 'link',
onSubmit: () => {
copyFileShareLink({
token: commandProps.authToken,
code: codeManager.code,
name: commandProps.projectData.project?.name || '',
units: commandProps.settings.defaultUnit,
}).catch(reportRejection)
},
},
] ]
} }

View File

@ -1,16 +0,0 @@
import { createCreateFileUrl } from './links'
describe(`link creation tests`, () => {
test(`createCreateFileUrl happy path`, async () => {
const code = `extrusionDistance = 12`
const name = `test`
const units = `mm`
// Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `http://localhost:3000/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
const result = createCreateFileUrl({ code, name, units })
expect(result.toString()).toBe(expectedLink)
})
})

View File

@ -1,100 +0,0 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import {
ASK_TO_OPEN_QUERY_PARAM,
CREATE_FILE_URL_PARAM,
PROD_APP_URL,
} from './constants'
import { stringToBase64 } from './base64'
import { DEV, VITE_KC_API_BASE_URL } from 'env'
import toast from 'react-hot-toast'
import { err } from './trap'
export interface FileLinkParams {
code: string
name: string
units: UnitLength_type
}
export async function copyFileShareLink(
args: FileLinkParams & { token: string }
) {
const token = args.token
if (!token) {
toast.error('You need to be signed in to share a file.', {
duration: 5000,
})
return
}
const shareUrl = createCreateFileUrl(args)
const shortlink = await createShortlink(token, shareUrl.toString())
if (err(shortlink)) {
toast.error(shortlink.message, {
duration: 5000,
})
return
}
await globalThis.navigator.clipboard.writeText(shortlink.url)
toast.success(
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!',
{
duration: 5000,
}
)
}
/**
* Creates a URL with the necessary query parameters to trigger
* the "Import file from URL" command in the app.
*
* With the additional step of asking the user if they want to
* open the URL in the desktop app.
*/
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
// Use the dev server if we are in development mode
let origin = DEV ? 'http://localhost:3000' : PROD_APP_URL
const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: String(true),
name,
units,
code: stringToBase64(code),
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
})
const createFileUrl = new URL(`?${searchParams.toString()}`, origin)
return createFileUrl
}
/**
* Given a file's code, name, and units, creates shareable link to the
* web app with a query parameter that triggers a modal to "open in desktop app".
* That modal is defined in the `OpenInDesktopAppHandler` component.
* TODO: update the return type to use TS library after its updated
*/
export async function createShortlink(
token: string,
url: string
): Promise<Error | { key: string; url: string }> {
/**
* We don't use our `withBaseURL` function here because
* there is no URL shortener service in the dev API.
*/
const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, {
method: 'POST',
headers: {
'Content-type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
url,
// In future we can support org-scoped and password-protected shortlinks here
// https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript
}),
})
if (!response.ok) {
const error = await response.json()
return new Error(`Failed to create shortlink: ${error.message}`)
} else {
return response.json()
}
}

View File

@ -114,7 +114,7 @@ export const fileLoader: LoaderFunction = async (
return redirect( return redirect(
`${PATHS.FILE}/${encodeURIComponent( `${PATHS.FILE}/${encodeURIComponent(
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
)}${new URL(routerData.request.url).search || ''}` )}`
) )
} }
@ -188,14 +188,11 @@ export const fileLoader: LoaderFunction = async (
// Loads the settings and by extension the projects in the default directory // Loads the settings and by extension the projects in the default directory
// and returns them to the Home route, along with any errors that occurred // and returns them to the Home route, along with any errors that occurred
export const homeLoader: LoaderFunction = async ({ export const homeLoader: LoaderFunction = async (): Promise<
request, HomeLoaderData | Response
}): Promise<HomeLoaderData | Response> => { > => {
const url = new URL(request.url)
if (!isDesktop()) { if (!isDesktop()) {
return redirect( return redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
)
} }
return {} return {}
} }

View File

@ -195,7 +195,7 @@ export async function submitAndAwaitTextToKcl({
.toLowerCase()}${FILE_EXT}` .toLowerCase()}${FILE_EXT}`
if (isDesktop()) { if (isDesktop()) {
// We have to preemptively run our unique file name logic, // We have to pre-emptively run our unique file name logic,
// so that we can pass the unique file name to the toast, // so that we can pass the unique file name to the toast,
// and by extension the file-deletion-on-reject logic. // and by extension the file-deletion-on-reject logic.
newFileName = getNextFileName({ newFileName = getNextFileName({

View File

@ -280,12 +280,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available', status: 'available',
title: 'Offset plane', title: 'Offset plane',
description: 'Create a plane parallel to an existing plane.', description: 'Create a plane parallel to an existing plane.',
links: [ links: [],
{
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/offsetPlane',
},
],
}, },
{ {
id: 'plane-points', id: 'plane-points',
@ -310,12 +305,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available', status: 'available',
title: 'Text-to-CAD', title: 'Text-to-CAD',
description: 'Generate geometry from a text prompt.', description: 'Generate geometry from a text prompt.',
links: [ links: [],
{
label: 'API docs',
url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text',
},
],
}, },
{ {
id: 'prompt-to-edit', id: 'prompt-to-edit',

View File

@ -25,10 +25,6 @@ export const projectsMachine = setup({
type: 'Delete project' type: 'Delete project'
data: ProjectsCommandSchema['Delete project'] data: ProjectsCommandSchema['Delete project']
} }
| {
type: 'Import file from URL'
data: ProjectsCommandSchema['Import file from URL']
}
| { type: 'navigate'; data: { name: string } } | { type: 'navigate'; data: { name: string } }
| { | {
type: 'xstate.done.actor.read-projects' type: 'xstate.done.actor.read-projects'
@ -46,10 +42,6 @@ export const projectsMachine = setup({
type: 'xstate.done.actor.rename-project' type: 'xstate.done.actor.rename-project'
output: { message: string; oldName: string; newName: string } output: { message: string; oldName: string; newName: string }
} }
| {
type: 'xstate.done.actor.create-file'
output: { message: string; projectName: string; fileName: string }
}
| { type: 'assign'; data: { [key: string]: any } }, | { type: 'assign'; data: { [key: string]: any } },
input: {} as { input: {} as {
projects: Project[] projects: Project[]
@ -68,7 +60,6 @@ export const projectsMachine = setup({
toastError: () => {}, toastError: () => {},
navigateToProject: () => {}, navigateToProject: () => {},
navigateToProjectIfNeeded: () => {}, navigateToProjectIfNeeded: () => {},
navigateToFile: () => {},
}, },
actors: { actors: {
readProjects: fromPromise(() => Promise.resolve([] as Project[])), readProjects: fromPromise(() => Promise.resolve([] as Project[])),
@ -99,22 +90,12 @@ export const projectsMachine = setup({
name: '', name: '',
}) })
), ),
createFile: fromPromise(
(_: {
input: ProjectsCommandSchema['Import file from URL'] & {
projects: Project[]
}
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
),
}, },
guards: { guards: {
'Has at least 1 project': ({ event }) => { 'Has at least 1 project': () => false,
if (event.type !== 'xstate.done.actor.read-projects') return false
return event.output.length ? event.output.length >= 1 : false
},
}, },
}).createMachine({ }).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjAHYAbADoArBJVMFTCQA4mTAMwmxAXwsy0mHARLkKASXRcATjywAzYgBtsb3cMLABVACUAGWY2JBAuXn5BONEESRl5BAUFFQM1HQUxJSYATmyFAyUTKxsMbDwiMjA1ZGosUlQsDmCAKzB8HlgKcLBcCC7e-sGYoQTiPgEhVIUy9SKSiQ0NEwkclRyMxGKJNRKlMRVDU23zpRqQW3qHJpa2jonUPoGhgGF3UZ42G6nymMzicwWyVAyzKJzECg0OiYGjERhK+kOWUMSlOGiUlTEJlMxRKBnuj3sjXIr1gHy+g2Go3GwPpsDBnG48ySS0QGj0+Q0JTOGkqBg2JhKmNRJy2OyUEn0Kml5LqlMczVatJZUyGI1IuDs2oG7PinMhPIQfKYAqFShF+PFkrkRwkYjU8OUShKKhMKg0pUs1geqoa6ppdJ1FD+AKBk2NrFmZu5KV5-L9tvtYokEsxelRagUCoMiMumyUdpVdlDL01Ee+FAAImAAoC6zwTRDk9DU9b08LRY7MWd1AZcoS-aoLiYFJWnlSNW0jQyAPIcMCkNsdpOLFOWtOC-sO7NOzLbGWFbY6acmFESWdql7R3B8UhQNsUCACZpkABuqAA1s0+D-M+YAALRLluiQ7t2WRMGIbryjeBgGBIEglNsCi5sY6hMOchQqEoCLFCh97VtST4vm+S4UGA7jBO4agcH4z7eKg7joGowExhBcbtgm4LblCIiKPBiHZiKqHoZhuaFhoBbGMYBhiPBCgStUQYUuRzR6gaZDUXxH5fmov4Ac0-z6pgvEgvGsQctBwnLGJahIZJaEYdOmKEfJuQSoRRKSRcZHPNSunoPp750QxTEsTwbEcWoFkGuBkECfZXIwSJcEIS5Ekoe5MnOggXqIaUqFiiUhIInemkhiFzRNi2EU0Z+1KmYBagQM2YCAtZ9JQRljmiTlrn5dJnlFShOLwcpZjKBs+KBrUVb1WojU9c1hlRexMWsexnFdS2KV8QN5q7laNqHlmOaTcpmjoWc8L6HoYrBfOagjGMm02QyrXfqQf4dSBEB9Tqp1dllebichUkeVhRXTiU+QecUKhCps4pvWGn0QN9rJGW1ANmYlTKg98DAKHZpoORanoyqh8oImU3pKJifJ5Ohdr7CYqjIvKWMvDjeORttjHMXtCXA2T0xpdTg20+W9MSIzgorIRXlpr6ORbPs+jwQLFEgVRPj+JQf0mUTHXcaBYG+AE4OZcsxbuts5hbCK+zFmImJgSc5zPV6BQVD6RQG80lERXblCi7tcX7VxRvgVHDtDQgaE4vK2gXKiTA6ItubmJo8IoqOylERUVhBh0XXwHEWn1YmNO7mBKg+2KpzK2IpIBUwBhqUtwYre9tbvEutfpWdsFFnkPpVJnQqz15Ozur36FoypREbGH4Zj438u7tO1qIu5qK5PBGyYjebpEkS44e9oVTbxHr5tnvk9ZeXajTuW+zaHNuzYRUmoeCEp-RKlUHJbeYVhYDDfhDVIxR5IGGnISQk6E+6EkxMUBQX9c66EMMg3Q5Zt7rWNkuOBjsjilDUAqQsZQzzgOkJNUk7o7SmF-tiXOUCmQwMGBQ1OF4TgVGzFUG8yIxQGDZiYPI4oqh+mPsrDSy05xhmfm+KO-CLT+iRucEUuwFQqWzMWXM2YTAuTtL6ZBylkRcMrkAA */ /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjBIkA2AHQAOCUw0qNkjQE4xYjQF8zMtJhwES5NcmpZSqLBwBOqAFZh8PWAoAJTBcCHcvX39YZjYkEC5efkF40QQFFQBmAHY1Qwz9QwBWU0NMrRl5BGyxBTUVJlVM01rtBXNLEGtsPCIyMEdnVwifPwCKAGEPUJ5sT1H-WKFE4j4BITSMzMzNIsyJDSZMhSYmFWzKxAkTNSYxIuU9zOKT7IsrDB67fsHYEajxiEwv8xjFWMtuKtkhsrpJboZsioincFMdTIjLggVGJ1GImMVMg0JISjEV3l1PrY+g4nH95gDAiFSLgbPSxkt4is1ilQGlrhJ4YjkbU0RoMXJEIYkWoikV8hJinjdOdyd0qfYBrSQdFJtNcLNtTwOZxIdyYQh+YKkSjReKqqYBQoEQpsgiSi9MqrKb0Nb9DYEACJgAA2YANbMW4M5puhqVhAvxQptCnRKkxCleuw0Gm2+2aRVRXpsPp+Woj4wA8hwwKRDcaEjH1nGLXDE9aRSmxWmJVipWocmdsbL8kxC501SWHFMZmQoIaKBABAMyAA3VAAawG+D1swAtOX61zY7zENlEWoMkoatcdPoipjCWIZRIirozsPiYYi19qQNp-rZ3nMAPC8Dw1A4YN9QAM1QDx0DUbcZjAfdInZKMTSSJsT2qc9Lwka8lTvTFTB2WUMiyQwc3yPRv3VH4mRZQDywXJc1FXDcBmmZlMBQhYjXQhtMJ5ERT1wlQr0kQj7kxKiZTxM5s2zbQEVoycBgY9AmNQ-wKGA0DwMgngYLgtQuJZZCDwEo8sJEnD1Dwgjb2kntig0GUkWVfZDEMfFVO+Bwg1DPhSDnZjFwcdjNzUCAQzDCztP4uIMKhGy0jPezxPwySnPvHsTjlPIhyOHRsnwlM-N-NRArDLS+N0kDYIM6DYPgmKgvivjD0bYS+VbBF21RTs7UUUcimfRpXyYRF8LFCrfSBCBaoZFiItINcor1CBeIZLqhPNdKL0yxzs2cqoNDqdpSo0EoSoLOb6NCRaQv9FblzWjjTMe7bQQYBQksElKesUMRjFubRMllCHsm2a5MVldRDBfFpmj0IpxPuhwFqW0F6v0iDmpMzbvuiXbAfNFNQcaI5IaKaH9jETFsUMNRVDxOU5TxBULE6VwYvgeIJ38sAIT25td27KpdzG7yZeho5slHVmMc1IY3HLfnkrNZt2hOW5smRCQM2uhQHkZ6VtkRBFnmu0qFGVv11ZFsnm0kdN8QFJVjlUPZ9co+3-2C0KEqdrXsJMJ8ryc86iUyYiCvxFNzldb3jHtjTsf8EPj1ssQchZlR8jR5EmDRrIGZcuF7maF1aZtslx29IWqtiwPDSz1LxDxepnlOfYDghp03eyOpngzXuYdHNPHozgJ26B9IXR2cTvP0BVkSRXKqgLtyq8OfQcXOwlubMIA */
id: 'Home machine', id: 'Home machine',
initial: 'Reading projects', initial: 'Reading projects',
@ -130,8 +111,6 @@ export const projectsMachine = setup({
})), })),
target: '.Reading projects', target: '.Reading projects',
}, },
'Import file from URL': '.Creating file',
}, },
states: { states: {
'Has no projects': { 'Has no projects': {
@ -176,10 +155,7 @@ export const projectsMachine = setup({
id: 'create-project', id: 'create-project',
src: 'createProject', src: 'createProject',
input: ({ event, context }) => { input: ({ event, context }) => {
if ( if (event.type !== 'Create project') {
event.type !== 'Create project' &&
event.type !== 'Import file from URL'
) {
return { return {
name: '', name: '',
projects: context.projects, projects: context.projects,
@ -296,39 +272,5 @@ export const projectsMachine = setup({
], ],
}, },
}, },
'Creating file': {
invoke: {
id: 'create-file',
src: 'createFile',
input: ({ event, context }) => {
if (event.type !== 'Import file from URL') {
return {
code: '',
name: '',
units: 'mm',
method: 'existingProject',
projects: context.projects,
}
}
return {
code: event.data.code || '',
name: event.data.name,
units: event.data.units,
method: event.data.method,
projectName: event.data.projectName,
projects: context.projects,
}
},
onDone: {
target: 'Reading projects',
actions: ['navigateToFile', 'toastSuccess'],
},
onError: {
target: 'Reading projects',
actions: 'toastError',
},
},
},
}, },
}) })

View File

@ -21,7 +21,6 @@ import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile' import getCurrentProjectFile from 'lib/getCurrentProjectFile'
import os from 'node:os' import os from 'node:os'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
import argvFromYargs from './commandLineArgs' import argvFromYargs from './commandLineArgs'
import * as packageJSON from '../package.json' import * as packageJSON from '../package.json'
@ -43,13 +42,15 @@ if (!process.env.NODE_ENV)
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] }) dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
process.env.VITE_KC_API_WS_MODELING_URL ??= process.env.VITE_KC_API_WS_MODELING_URL ??=
'wss://api.dev.zoo.dev/ws/modeling/commands' 'wss://api.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.dev.zoo.dev' process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
process.env.VITE_KC_SITE_BASE_URL ??= 'https://dev.zoo.dev' process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
process.env.VITE_KC_SKIP_AUTH ??= 'false' process.env.VITE_KC_SKIP_AUTH ??= 'false'
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000' process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
/// Register our application to handle all "zoo-studio:" protocols. const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
/// Register our application to handle all "electron-fiddle://" protocols.
if (process.defaultApp) { if (process.defaultApp) {
if (process.argv.length >= 2) { if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [ app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
@ -64,7 +65,7 @@ if (process.defaultApp) {
// Must be done before ready event. // Must be done before ready event.
registerStartupListeners() registerStartupListeners()
const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => { const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
let newWindow let newWindow
if (reuse) { if (reuse) {
@ -89,34 +90,11 @@ const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
}) })
} }
const pathIsCustomProtocolLink =
pathToOpen?.startsWith(ZOO_STUDIO_PROTOCOL) ?? false
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
const filteredPath = pathToOpen newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
: ''
const fullHashBasedUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/${filteredPath}`
newWindow.loadURL(fullHashBasedUrl).catch(reportRejection)
} else { } else {
if (pathIsCustomProtocolLink && pathToOpen) { getProjectPathAtStartup(filePath)
// We're trying to open a custom protocol link
const filteredPath = pathToOpen
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
: ''
const startIndex = path.join(
__dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
)
newWindow
.loadFile(startIndex, {
hash: filteredPath,
})
.catch(reportRejection)
} else {
// otherwise we're trying to open a local file from the command line
getProjectPathAtStartup(pathToOpen)
.then(async (projectPath) => { .then(async (projectPath) => {
const startIndex = path.join( const startIndex = path.join(
__dirname, __dirname,
@ -128,6 +106,8 @@ const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
return return
} }
console.log('Loading file', projectPath)
const fullUrl = `/file/${encodeURIComponent(projectPath)}` const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl) console.log('Full URL', fullUrl)
@ -137,7 +117,6 @@ const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
}) })
.catch(reportRejection) .catch(reportRejection)
} }
}
// Open the DevTools. // Open the DevTools.
// mainWindow.webContents.openDevTools() // mainWindow.webContents.openDevTools()

View File

@ -25,7 +25,6 @@ import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader' import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { useProjectsContext } from 'hooks/useProjectsContext' import { useProjectsContext } from 'hooks/useProjectsContext'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
// This route only opens in the desktop context for now, // This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types. // as defined in Router.tsx, so we can use the desktop APIs and types.
@ -35,18 +34,6 @@ const Home = () => {
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0) const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger]) const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery((argDefaultValues) => {
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'projects',
name: 'Import file from URL',
argDefaultValues,
},
})
})
useRefreshSettings(PATHS.HOME + 'SETTINGS') useRefreshSettings(PATHS.HOME + 'SETTINGS')
const navigate = useNavigate() const navigate = useNavigate()
const { const {
@ -161,7 +148,7 @@ const Home = () => {
}} }}
data-testid="home-new-file" data-testid="home-new-file"
> >
Create project New project
</ActionButton> </ActionButton>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">

View File

@ -1382,6 +1382,12 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "iai"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.61" version = "0.1.61"
@ -1733,6 +1739,7 @@ dependencies = [
"gltf-json", "gltf-json",
"handlebars", "handlebars",
"http 1.2.0", "http 1.2.0",
"iai",
"image", "image",
"indexmap 2.7.0", "indexmap 2.7.0",
"insta", "insta",

View File

@ -113,6 +113,7 @@ base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] } criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0" expectorate = "1.1.0"
handlebars = "6.3.0" handlebars = "6.3.0"
iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] } image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] } insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0" itertools = "0.13.0"
@ -128,6 +129,10 @@ workspace = true
name = "compiler_benchmark_criterion" name = "compiler_benchmark_criterion"
harness = false harness = false
[[bench]]
name = "compiler_benchmark_iai"
harness = false
[[bench]] [[bench]]
name = "digest_benchmark" name = "digest_benchmark"
harness = false harness = false
@ -137,7 +142,15 @@ name = "lsp_semantic_tokens_benchmark_criterion"
harness = false harness = false
required-features = ["lsp-test-util"] required-features = ["lsp-test-util"]
[[bench]]
name = "lsp_semantic_tokens_benchmark_iai"
harness = false
required-features = ["lsp-test-util"]
[[bench]] [[bench]]
name = "executor_benchmark_criterion" name = "executor_benchmark_criterion"
harness = false harness = false
[[bench]]
name = "executor_benchmark_iai"
harness = false

View File

@ -0,0 +1,35 @@
use iai::black_box;
pub fn parse(program: &str) {
black_box(kcl_lib::Program::parse(program).unwrap());
}
fn parse_kitt() {
parse(KITT_PROGRAM)
}
fn parse_pipes() {
parse(PIPES_PROGRAM)
}
fn parse_cube() {
parse(CUBE_PROGRAM)
}
fn parse_math() {
parse(MATH_PROGRAM)
}
fn parse_lsystem() {
parse(LSYSTEM_PROGRAM)
}
iai::main! {
parse_kitt,
parse_pipes,
parse_cube,
parse_math,
parse_lsystem,
}
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");

View File

@ -0,0 +1,27 @@
use iai::black_box;
async fn execute_server_rack_heavy() {
let code = SERVER_RACK_HEAVY_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await
.unwrap(),
);
}
async fn execute_server_rack_lite() {
let code = SERVER_RACK_LITE_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await
.unwrap(),
);
}
iai::main! {
execute_server_rack_lite,
execute_server_rack_heavy,
}
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");
const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl");

View File

@ -0,0 +1,45 @@
use iai::black_box;
use kcl_lib::kcl_lsp_server;
use tower_lsp::LanguageServer;
async fn kcl_lsp_semantic_tokens(code: &str) {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: code.to_string(),
},
})
.await;
// Send semantic tokens request.
black_box(
server
.semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
.await
.unwrap()
.unwrap(),
);
}
async fn semantic_tokens_global_tags() {
let code = GLOBAL_TAGS_FILE;
kcl_lsp_semantic_tokens(code).await;
}
iai::main! {
semantic_tokens_global_tags,
}
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");

View File

@ -370,6 +370,8 @@ impl From<KclError> for pyo3::PyErr {
pub struct CompilationError { pub struct CompilationError {
#[serde(rename = "sourceRange")] #[serde(rename = "sourceRange")]
pub source_range: SourceRange, pub source_range: SourceRange,
#[serde(rename = "contextRange")]
pub context_range: Option<SourceRange>,
pub message: String, pub message: String,
pub suggestion: Option<Suggestion>, pub suggestion: Option<Suggestion>,
pub severity: Severity, pub severity: Severity,
@ -380,6 +382,7 @@ impl CompilationError {
pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError { pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
CompilationError { CompilationError {
source_range, source_range,
context_range: None,
message: message.to_string(), message: message.to_string(),
suggestion: None, suggestion: None,
severity: Severity::Error, severity: Severity::Error,
@ -390,6 +393,7 @@ impl CompilationError {
pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError { pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
CompilationError { CompilationError {
source_range, source_range,
context_range: None,
message: message.to_string(), message: message.to_string(),
suggestion: None, suggestion: None,
severity: Severity::Fatal, severity: Severity::Fatal,
@ -398,18 +402,22 @@ impl CompilationError {
} }
pub(crate) fn with_suggestion( pub(crate) fn with_suggestion(
self, source_range: SourceRange,
suggestion_title: impl ToString, context_range: Option<SourceRange>,
suggestion_insert: impl ToString, message: impl ToString,
suggestion: Option<(impl ToString, impl ToString)>,
tag: Tag, tag: Tag,
) -> CompilationError { ) -> CompilationError {
CompilationError { CompilationError {
suggestion: Some(Suggestion { source_range,
title: suggestion_title.to_string(), context_range,
insert: suggestion_insert.to_string(), message: message.to_string(),
suggestion: suggestion.map(|(t, i)| Suggestion {
title: t.to_string(),
insert: i.to_string(),
}), }),
severity: Severity::Error,
tag, tag,
..self
} }
} }

View File

@ -11,11 +11,6 @@ pub(super) const SETTINGS: &str = "settings";
pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit"; pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit"; pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum AnnotationScope {
Module,
}
pub(super) fn expect_properties<'a>( pub(super) fn expect_properties<'a>(
for_key: &'static str, for_key: &'static str,
annotation: &'a NonCodeValue, annotation: &'a NonCodeValue,

View File

@ -121,8 +121,8 @@ impl Node<MemberExpression> {
source_ranges: vec![self.clone().into()], source_ranges: vec![self.clone().into()],
})) }))
} }
(KclValue::Solid { value }, Property::String(prop)) if prop == "sketch" => Ok(KclValue::Sketch { (KclValue::Solid(solid), Property::String(prop)) if prop == "sketch" => Ok(KclValue::Sketch {
value: Box::new(value.sketch), value: Box::new(solid.sketch),
}), }),
(KclValue::Sketch { value: sk }, Property::String(prop)) if prop == "tags" => Ok(KclValue::Object { (KclValue::Sketch { value: sk }, Property::String(prop)) if prop == "tags" => Ok(KclValue::Object {
meta: vec![Metadata { meta: vec![Metadata {
@ -662,11 +662,11 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
exec_state.mut_memory().update_tag(&tag.value, tag.clone())?; exec_state.mut_memory().update_tag(&tag.value, tag.clone())?;
} }
} }
KclValue::Solid { ref mut value } => { KclValue::Solid(ref mut solid) => {
for v in &value.value { for value in &solid.value {
if let Some(tag) = v.get_tag() { if let Some(tag) = value.get_tag() {
// Get the past tag and update it. // Get the past tag and update it.
let mut t = if let Some(t) = value.sketch.tags.get(&tag.name) { let mut t = if let Some(t) = solid.sketch.tags.get(&tag.name) {
t.clone() t.clone()
} else { } else {
// It's probably a fillet or a chamfer. // It's probably a fillet or a chamfer.
@ -674,10 +674,10 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
TagIdentifier { TagIdentifier {
value: tag.name.clone(), value: tag.name.clone(),
info: Some(TagEngineInfo { info: Some(TagEngineInfo {
id: v.get_id(), id: value.get_id(),
surface: Some(v.clone()), surface: Some(value.clone()),
path: None, path: None,
sketch: value.id, sketch: solid.id,
}), }),
meta: vec![Metadata { meta: vec![Metadata {
source_range: tag.clone().into(), source_range: tag.clone().into(),
@ -693,21 +693,21 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
}; };
let mut info = info.clone(); let mut info = info.clone();
info.surface = Some(v.clone()); info.surface = Some(value.clone());
info.sketch = value.id; info.sketch = solid.id;
t.info = Some(info); t.info = Some(info);
exec_state.mut_memory().update_tag(&tag.name, t.clone())?; exec_state.mut_memory().update_tag(&tag.name, t.clone())?;
// update the sketch tags. // update the sketch tags.
value.sketch.tags.insert(tag.name.clone(), t); solid.sketch.tags.insert(tag.name.clone(), t);
} }
} }
// Find the stale sketch in memory and update it. // Find the stale sketch in memory and update it.
let cur_env_index = exec_state.memory().current_env.index(); let cur_env_index = exec_state.memory().current_env.index();
if let Some(current_env) = exec_state.mut_memory().environments.get_mut(cur_env_index) { if let Some(current_env) = exec_state.mut_memory().environments.get_mut(cur_env_index) {
current_env.update_sketch_tags(&value.sketch); current_env.update_sketch_tags(&solid.sketch);
} }
} }
_ => {} _ => {}
@ -929,13 +929,13 @@ impl Property {
LiteralIdentifier::Literal(literal) => { LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone(); let value = literal.value.clone();
match value { match value {
LiteralValue::Number { value, .. } => { LiteralValue::Number(x) => {
if let Some(x) = crate::try_f64_to_usize(value) { if let Some(x) = crate::try_f64_to_usize(x) {
Ok(Property::UInt(x)) Ok(Property::UInt(x))
} else { } else {
Err(KclError::Semantic(KclErrorDetails { Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr, source_ranges: property_sr,
message: format!("{value} is not a valid index, indices must be whole numbers >= 0"), message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
})) }))
} }
} }

View File

@ -62,27 +62,19 @@ pub enum KclValue {
}, },
TagIdentifier(Box<TagIdentifier>), TagIdentifier(Box<TagIdentifier>),
TagDeclarator(crate::parsing::ast::types::BoxNode<TagDeclarator>), TagDeclarator(crate::parsing::ast::types::BoxNode<TagDeclarator>),
Plane { Plane(Box<Plane>),
value: Box<Plane>, Face(Box<Face>),
},
Face {
value: Box<Face>,
},
Sketch { Sketch {
value: Box<Sketch>, value: Box<Sketch>,
}, },
Sketches { Sketches {
value: Vec<Box<Sketch>>, value: Vec<Box<Sketch>>,
}, },
Solid { Solid(Box<Solid>),
value: Box<Solid>,
},
Solids { Solids {
value: Vec<Box<Solid>>, value: Vec<Box<Solid>>,
}, },
Helix { Helix(Box<Helix>),
value: Box<Helix>,
},
ImportedGeometry(ImportedGeometry), ImportedGeometry(ImportedGeometry),
#[ts(skip)] #[ts(skip)]
Function { Function {
@ -128,7 +120,7 @@ impl From<Vec<Box<Sketch>>> for KclValue {
impl From<SolidSet> for KclValue { impl From<SolidSet> for KclValue {
fn from(eg: SolidSet) -> Self { fn from(eg: SolidSet) -> Self {
match eg { match eg {
SolidSet::Solid(eg) => KclValue::Solid { value: eg }, SolidSet::Solid(eg) => KclValue::Solid(eg),
SolidSet::Solids(egs) => KclValue::Solids { value: egs }, SolidSet::Solids(egs) => KclValue::Solids { value: egs },
} }
} }
@ -137,7 +129,7 @@ impl From<SolidSet> for KclValue {
impl From<Vec<Box<Solid>>> for KclValue { impl From<Vec<Box<Solid>>> for KclValue {
fn from(eg: Vec<Box<Solid>>) -> Self { fn from(eg: Vec<Box<Solid>>) -> Self {
if eg.len() == 1 { if eg.len() == 1 {
KclValue::Solid { value: eg[0].clone() } KclValue::Solid(eg[0].clone())
} else { } else {
KclValue::Solids { value: eg } KclValue::Solids { value: eg }
} }
@ -148,15 +140,15 @@ impl From<KclValue> for Vec<SourceRange> {
match item { match item {
KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)], KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta), KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid { value } => to_vec_sr(&value.meta), KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(), KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta), KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(), KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Helix { value } => to_vec_sr(&value.meta), KclValue::Helix(e) => to_vec_sr(&e.meta),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta), KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(&meta), KclValue::Function { meta, .. } => to_vec_sr(&meta),
KclValue::Plane { value } => to_vec_sr(&value.meta), KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Face { value } => to_vec_sr(&value.meta), KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Bool { meta, .. } => to_vec_sr(&meta), KclValue::Bool { meta, .. } => to_vec_sr(&meta),
KclValue::Number { meta, .. } => to_vec_sr(&meta), KclValue::Number { meta, .. } => to_vec_sr(&meta),
KclValue::Int { meta, .. } => to_vec_sr(&meta), KclValue::Int { meta, .. } => to_vec_sr(&meta),
@ -179,15 +171,15 @@ impl From<&KclValue> for Vec<SourceRange> {
match item { match item {
KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)], KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta), KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid { value } => to_vec_sr(&value.meta), KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(), KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta), KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(), KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Helix { value } => to_vec_sr(&value.meta), KclValue::Helix(x) => to_vec_sr(&x.meta),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta), KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(meta), KclValue::Function { meta, .. } => to_vec_sr(meta),
KclValue::Plane { value } => to_vec_sr(&value.meta), KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Face { value } => to_vec_sr(&value.meta), KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Bool { meta, .. } => to_vec_sr(meta), KclValue::Bool { meta, .. } => to_vec_sr(meta),
KclValue::Number { meta, .. } => to_vec_sr(meta), KclValue::Number { meta, .. } => to_vec_sr(meta),
KclValue::Int { meta, .. } => to_vec_sr(meta), KclValue::Int { meta, .. } => to_vec_sr(meta),
@ -213,13 +205,13 @@ impl KclValue {
KclValue::Object { value: _, meta } => meta.clone(), KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(), KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()], KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane { value } => value.meta.clone(), KclValue::Plane(x) => x.meta.clone(),
KclValue::Face { value } => value.meta.clone(), KclValue::Face(x) => x.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(), KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(), KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid { value } => value.meta.clone(), KclValue::Solid(x) => x.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(), KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Helix { value } => value.meta.clone(), KclValue::Helix(x) => x.meta.clone(),
KclValue::ImportedGeometry(x) => x.meta.clone(), KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(), KclValue::Function { meta, .. } => meta.clone(),
KclValue::Module { meta, .. } => meta.clone(), KclValue::Module { meta, .. } => meta.clone(),
@ -238,7 +230,7 @@ impl KclValue {
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> { pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self { match self {
KclValue::Solid { value } => Ok(SolidSet::Solid(value.clone())), KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())), KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::Array { value, .. } => { KclValue::Array { value, .. } => {
let solids: Vec<_> = value let solids: Vec<_> = value
@ -274,15 +266,15 @@ impl KclValue {
KclValue::Uuid { .. } => "Unique ID (uuid)", KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator", KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier", KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid { .. } => "Solid", KclValue::Solid(_) => "Solid",
KclValue::Solids { .. } => "Solids", KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch", KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches", KclValue::Sketches { .. } => "Sketches",
KclValue::Helix { .. } => "Helix", KclValue::Helix(_) => "Helix",
KclValue::ImportedGeometry(_) => "ImportedGeometry", KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function", KclValue::Function { .. } => "Function",
KclValue::Plane { .. } => "Plane", KclValue::Plane(_) => "Plane",
KclValue::Face { .. } => "Face", KclValue::Face(_) => "Face",
KclValue::Bool { .. } => "boolean (true/false value)", KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number", KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer", KclValue::Int { .. } => "integer",
@ -296,7 +288,7 @@ impl KclValue {
pub(crate) fn from_literal(literal: LiteralValue, meta: Vec<Metadata>) -> Self { pub(crate) fn from_literal(literal: LiteralValue, meta: Vec<Metadata>) -> Self {
match literal { match literal {
LiteralValue::Number { value, .. } => KclValue::Number { value, meta }, LiteralValue::Number(value) => KclValue::Number { value, meta },
LiteralValue::String(value) => KclValue::String { value, meta }, LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta }, LiteralValue::Bool(value) => KclValue::Bool { value, meta },
} }
@ -391,7 +383,7 @@ impl KclValue {
} }
pub fn as_plane(&self) -> Option<&Plane> { pub fn as_plane(&self) -> Option<&Plane> {
if let KclValue::Plane { value } = &self { if let KclValue::Plane(value) = &self {
Some(value) Some(value)
} else { } else {
None None
@ -399,7 +391,7 @@ impl KclValue {
} }
pub fn as_solid(&self) -> Option<&Solid> { pub fn as_solid(&self) -> Option<&Solid> {
if let KclValue::Solid { value } = &self { if let KclValue::Solid(value) = &self {
Some(value) Some(value)
} else { } else {
None None
@ -622,19 +614,6 @@ impl From<crate::UnitLength> for UnitLen {
} }
} }
impl From<UnitLen> for crate::UnitLength {
fn from(unit: UnitLen) -> Self {
match unit {
UnitLen::Cm => crate::UnitLength::Cm,
UnitLen::Feet => crate::UnitLength::Ft,
UnitLen::Inches => crate::UnitLength::In,
UnitLen::M => crate::UnitLength::M,
UnitLen::Mm => crate::UnitLength::Mm,
UnitLen::Yards => crate::UnitLength::Yd,
}
}
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]

View File

@ -2,7 +2,6 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use annotations::AnnotationScope;
use anyhow::Result; use anyhow::Result;
use artifact::build_artifact_graph; use artifact::build_artifact_graph;
use async_recursion::async_recursion; use async_recursion::async_recursion;
@ -392,7 +391,7 @@ impl ProgramMemory {
env.bindings env.bindings
.values() .values()
.filter_map(|item| match item { .filter_map(|item| match item {
KclValue::Solid { value } if value.sketch.id == sketch_id => Some(value.clone()), KclValue::Solid(eg) if eg.sketch.id == sketch_id => Some(eg.clone()),
_ => None, _ => None,
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -506,8 +505,8 @@ impl DynamicState {
fn append(&mut self, memory: &ProgramMemory) { fn append(&mut self, memory: &ProgramMemory) {
for env in &memory.environments { for env in &memory.environments {
for item in env.bindings.values() { for item in env.bindings.values() {
if let KclValue::Solid { value } = item { if let KclValue::Solid(eg) = item {
self.solid_ids.push(SolidLazyIds::from(value.as_ref())); self.solid_ids.push(SolidLazyIds::from(eg.as_ref()));
} }
} }
} }
@ -760,7 +759,6 @@ pub struct Helix {
pub angle_start: f64, pub angle_start: f64,
/// Is the helix rotation counter clockwise? /// Is the helix rotation counter clockwise?
pub ccw: bool, pub ccw: bool,
pub units: UnitLen,
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
pub meta: Vec<Metadata>, pub meta: Vec<Metadata>,
} }
@ -782,7 +780,6 @@ pub struct Plane {
pub y_axis: Point3d, pub y_axis: Point3d,
/// The z-axis (normal). /// The z-axis (normal).
pub z_axis: Point3d, pub z_axis: Point3d,
pub units: UnitLen,
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
pub meta: Vec<Metadata>, pub meta: Vec<Metadata>,
} }
@ -798,7 +795,6 @@ impl Plane {
y_axis: Point3d::new(0.0, 1.0, 0.0), y_axis: Point3d::new(0.0, 1.0, 0.0),
z_axis: Point3d::new(0.0, 0.0, 1.0), z_axis: Point3d::new(0.0, 0.0, 1.0),
value: PlaneType::XY, value: PlaneType::XY,
units: exec_state.length_unit(),
meta: vec![], meta: vec![],
}, },
crate::std::sketch::PlaneData::NegXY => Plane { crate::std::sketch::PlaneData::NegXY => Plane {
@ -808,7 +804,6 @@ impl Plane {
y_axis: Point3d::new(0.0, 1.0, 0.0), y_axis: Point3d::new(0.0, 1.0, 0.0),
z_axis: Point3d::new(0.0, 0.0, -1.0), z_axis: Point3d::new(0.0, 0.0, -1.0),
value: PlaneType::XY, value: PlaneType::XY,
units: exec_state.length_unit(),
meta: vec![], meta: vec![],
}, },
crate::std::sketch::PlaneData::XZ => Plane { crate::std::sketch::PlaneData::XZ => Plane {
@ -818,7 +813,6 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0), y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(0.0, -1.0, 0.0), z_axis: Point3d::new(0.0, -1.0, 0.0),
value: PlaneType::XZ, value: PlaneType::XZ,
units: exec_state.length_unit(),
meta: vec![], meta: vec![],
}, },
crate::std::sketch::PlaneData::NegXZ => Plane { crate::std::sketch::PlaneData::NegXZ => Plane {
@ -828,7 +822,6 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0), y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(0.0, 1.0, 0.0), z_axis: Point3d::new(0.0, 1.0, 0.0),
value: PlaneType::XZ, value: PlaneType::XZ,
units: exec_state.length_unit(),
meta: vec![], meta: vec![],
}, },
crate::std::sketch::PlaneData::YZ => Plane { crate::std::sketch::PlaneData::YZ => Plane {
@ -838,7 +831,6 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0), y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(1.0, 0.0, 0.0), z_axis: Point3d::new(1.0, 0.0, 0.0),
value: PlaneType::YZ, value: PlaneType::YZ,
units: exec_state.length_unit(),
meta: vec![], meta: vec![],
}, },
crate::std::sketch::PlaneData::NegYZ => Plane { crate::std::sketch::PlaneData::NegYZ => Plane {
@ -848,7 +840,6 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0), y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(-1.0, 0.0, 0.0), z_axis: Point3d::new(-1.0, 0.0, 0.0),
value: PlaneType::YZ, value: PlaneType::YZ,
units: exec_state.length_unit(),
meta: vec![], meta: vec![],
}, },
crate::std::sketch::PlaneData::Plane { crate::std::sketch::PlaneData::Plane {
@ -863,7 +854,6 @@ impl Plane {
y_axis: *y_axis, y_axis: *y_axis,
z_axis: *z_axis, z_axis: *z_axis,
value: PlaneType::Custom, value: PlaneType::Custom,
units: exec_state.length_unit(),
meta: vec![], meta: vec![],
}, },
} }
@ -910,7 +900,6 @@ pub struct Face {
pub z_axis: Point3d, pub z_axis: Point3d,
/// The solid the face is on. /// The solid the face is on.
pub solid: Box<Solid>, pub solid: Box<Solid>,
pub units: UnitLen,
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
pub meta: Vec<Metadata>, pub meta: Vec<Metadata>,
} }
@ -1029,7 +1018,6 @@ pub struct Sketch {
/// is sketched on face etc. /// is sketched on face etc.
#[serde(skip)] #[serde(skip)]
pub original_id: uuid::Uuid, pub original_id: uuid::Uuid,
pub units: UnitLen,
/// Metadata. /// Metadata.
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
pub meta: Vec<Metadata>, pub meta: Vec<Metadata>,
@ -1153,7 +1141,6 @@ pub struct Solid {
/// Chamfers or fillets on this solid. /// Chamfers or fillets on this solid.
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_cuts: Vec<EdgeCut>, pub edge_cuts: Vec<EdgeCut>,
pub units: UnitLen,
/// Metadata. /// Metadata.
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
pub meta: Vec<Metadata>, pub meta: Vec<Metadata>,
@ -2317,36 +2304,6 @@ impl ExecutorContext {
} }
} }
async fn handle_annotations(
&self,
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
scope: AnnotationScope,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
for (annotation, source_range) in annotations {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
if scope == AnnotationScope::Module {
let old_units = exec_state.length_unit();
exec_state
.mod_local
.settings
.update_from_annotation(annotation, source_range)?;
let new_units = exec_state.length_unit();
if old_units != new_units {
self.engine.set_units(new_units.into(), source_range).await?;
}
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Settings can only be modified at the top level scope of a file".to_owned(),
source_ranges: vec![source_range],
}));
}
}
// TODO warn on unknown annotations
}
Ok(())
}
/// Execute an AST's program. /// Execute an AST's program.
#[async_recursion] #[async_recursion]
pub(crate) async fn inner_execute<'a>( pub(crate) async fn inner_execute<'a>(
@ -2355,16 +2312,21 @@ impl ExecutorContext {
exec_state: &mut ExecState, exec_state: &mut ExecState,
body_type: BodyType, body_type: BodyType,
) -> Result<Option<KclValue>, KclError> { ) -> Result<Option<KclValue>, KclError> {
self.handle_annotations( if let Some((annotation, source_range)) = program
program
.non_code_meta .non_code_meta
.start_nodes .start_nodes
.iter() .iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))), .filter_map(|n| {
AnnotationScope::Module, n.annotation(annotations::SETTINGS)
exec_state, .map(|result| (result, n.as_source_range()))
) })
.await?; .next()
{
exec_state
.mod_local
.settings
.update_from_annotation(annotation, source_range)?;
}
let mut last_expr = None; let mut last_expr = None;
// Iterate over the body of the program. // Iterate over the body of the program.
@ -2547,7 +2509,6 @@ impl ExecutorContext {
exec_kind: ExecutionKind, exec_kind: ExecutionKind,
source_range: SourceRange, source_range: SourceRange,
) -> Result<(Option<KclValue>, ProgramMemory, Vec<String>), KclError> { ) -> Result<(Option<KclValue>, ProgramMemory, Vec<String>), KclError> {
let old_units = exec_state.length_unit();
// TODO It sucks that we have to clone the whole module AST here // TODO It sucks that we have to clone the whole module AST here
let info = exec_state.global.module_infos[&module_id].clone(); let info = exec_state.global.module_infos[&module_id].clone();
@ -2564,11 +2525,7 @@ impl ExecutorContext {
.inner_execute(&info.parsed.unwrap(), exec_state, crate::execution::BodyType::Root) .inner_execute(&info.parsed.unwrap(), exec_state, crate::execution::BodyType::Root)
.await; .await;
let new_units = exec_state.length_unit();
std::mem::swap(&mut exec_state.mod_local, &mut local_state); std::mem::swap(&mut exec_state.mod_local, &mut local_state);
if new_units != old_units {
self.engine.set_units(old_units.into(), Default::default()).await?;
}
self.engine.replace_execution_kind(original_execution); self.engine.replace_execution_kind(original_execution);
let result = result.map_err(|err| { let result = result.map_err(|err| {

View File

@ -163,7 +163,7 @@ fn get_xyz(point: &ObjectExpression) -> Option<(f64, f64, f64)> {
fn unlitafy(lit: &LiteralValue) -> Option<f64> { fn unlitafy(lit: &LiteralValue) -> Option<f64> {
Some(match lit { Some(match lit {
LiteralValue::Number { value, .. } => *value, LiteralValue::Number(value) => *value,
_ => { _ => {
return None; return None;
} }

View File

@ -1,6 +1,6 @@
use sha2::{Digest as DigestTrait, Sha256}; use sha2::{Digest as DigestTrait, Sha256};
use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, LiteralValue, VariableKind}; use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, VariableKind};
use crate::parsing::ast::types::{ use crate::parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression, ImportItem, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression, ImportItem,
@ -277,26 +277,6 @@ impl Literal {
}); });
} }
impl LiteralValue {
fn digestable_id(&self) -> Vec<u8> {
match self {
LiteralValue::Number { value, suffix } => {
let mut result: Vec<u8> = value.to_ne_bytes().into();
result.extend((*suffix as u32).to_ne_bytes());
result
}
LiteralValue::String(st) => st.as_bytes().into(),
LiteralValue::Bool(b) => {
if *b {
vec![1]
} else {
vec![0]
}
}
}
}
}
impl Identifier { impl Identifier {
compute_digest!(|slf, hasher| { compute_digest!(|slf, hasher| {
let name = slf.name.as_bytes(); let name = slf.name.as_bytes();

View File

@ -18,8 +18,6 @@ use crate::{
Program, Program,
}; };
use super::types::LiteralValue;
type Point3d = kcmc::shared::Point3d<f64>; type Point3d = kcmc::shared::Point3d<f64>;
#[derive(Debug)] #[derive(Debug)]
@ -203,8 +201,8 @@ fn create_start_sketch_on(
"startProfileAt", "startProfileAt",
vec![ vec![
ArrayExpression::new(vec![ ArrayExpression::new(vec![
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(start[0]))).into(), Literal::new(round_before_recast(start[0]).into()).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(start[1]))).into(), Literal::new(round_before_recast(start[1]).into()).into(),
]) ])
.into(), .into(),
PipeSubstitution::new().into(), PipeSubstitution::new().into(),
@ -223,8 +221,8 @@ fn create_start_sketch_on(
"line", "line",
vec![ vec![
ArrayExpression::new(vec![ ArrayExpression::new(vec![
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(end[0]))).into(), Literal::new(round_before_recast(end[0]).into()).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(end[1]))).into(), Literal::new(round_before_recast(end[1]).into()).into(),
]) ])
.into(), .into(),
PipeSubstitution::new().into(), PipeSubstitution::new().into(),
@ -256,8 +254,8 @@ fn create_start_sketch_on(
"line", "line",
vec![ vec![
ArrayExpression::new(vec![ ArrayExpression::new(vec![
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(line[0]))).into(), Literal::new(round_before_recast(line[0]).into()).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(line[1]))).into(), Literal::new(round_before_recast(line[1]).into()).into(),
]) ])
.into(), .into(),
PipeSubstitution::new().into(), PipeSubstitution::new().into(),

View File

@ -1,49 +1,31 @@
use std::fmt;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use super::Node; use super::Node;
use crate::parsing::{ use crate::parsing::ast::types::{Expr, Literal};
ast::types::{Expr, Literal},
token::NumericSuffix,
};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(untagged, rename_all = "snake_case")] #[serde(untagged, rename_all = "snake_case")]
pub enum LiteralValue { pub enum LiteralValue {
Number { value: f64, suffix: NumericSuffix }, Number(f64),
String(String), String(String),
Bool(bool), Bool(bool),
} }
impl LiteralValue { impl LiteralValue {
pub fn from_f64_no_uom(value: f64) -> Self { pub fn digestable_id(&self) -> Vec<u8> {
LiteralValue::Number {
value,
suffix: NumericSuffix::None,
}
}
}
impl fmt::Display for LiteralValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
LiteralValue::Number { value, suffix } => { LiteralValue::Number(frac) => frac.to_ne_bytes().into(),
let int_value = *value as u64; LiteralValue::String(st) => st.as_bytes().into(),
if int_value as f64 == *value { LiteralValue::Bool(b) => {
write!(f, "{int_value}")?; if *b {
vec![1]
} else { } else {
write!(f, "{value}")?; vec![0]
} }
if *suffix != NumericSuffix::None {
write!(f, "{suffix}")?;
} }
Ok(())
}
LiteralValue::String(s) => write!(f, "\"{s}\""),
LiteralValue::Bool(b) => write!(f, "{b}"),
} }
} }
} }
@ -54,12 +36,49 @@ impl From<Node<Literal>> for Expr {
} }
} }
impl From<LiteralValue> for JValue {
fn from(value: LiteralValue) -> Self {
match value {
LiteralValue::Number(x) => x.into(),
LiteralValue::String(x) => x.into(),
LiteralValue::Bool(b) => b.into(),
}
}
}
impl From<f64> for LiteralValue {
fn from(value: f64) -> Self {
Self::Number(value)
}
}
impl From<i64> for LiteralValue {
fn from(value: i64) -> Self {
Self::Number(value as f64)
}
}
impl From<String> for LiteralValue { impl From<String> for LiteralValue {
fn from(value: String) -> Self { fn from(value: String) -> Self {
Self::String(value) Self::String(value)
} }
} }
impl From<u32> for LiteralValue {
fn from(value: u32) -> Self {
Self::Number(value as f64)
}
}
impl From<u16> for LiteralValue {
fn from(value: u16) -> Self {
Self::Number(value as f64)
}
}
impl From<u8> for LiteralValue {
fn from(value: u8) -> Self {
Self::Number(value as f64)
}
}
impl From<&'static str> for LiteralValue { impl From<&'static str> for LiteralValue {
fn from(value: &'static str) -> Self { fn from(value: &'static str) -> Self {
// TODO: Make this Cow<str> // TODO: Make this Cow<str>

View File

@ -13,6 +13,7 @@ use anyhow::Result;
use parse_display::{Display, FromStr}; use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use tower_lsp::lsp_types::{ use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind, CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind,
}; };
@ -1011,9 +1012,9 @@ impl NonCodeNode {
} }
} }
pub fn annotation(&self) -> Option<&NonCodeValue> { pub fn annotation(&self, expected_name: &str) -> Option<&NonCodeValue> {
match &self.value { match &self.value {
a @ NonCodeValue::Annotation { .. } => Some(a), a @ NonCodeValue::Annotation { name, .. } if name.name == expected_name => Some(a),
_ => None, _ => None,
} }
} }
@ -1071,15 +1072,6 @@ pub enum NonCodeValue {
}, },
} }
impl NonCodeValue {
pub fn annotation_name(&self) -> Option<&str> {
match self {
NonCodeValue::Annotation { name, .. } => Some(&name.name),
_ => None,
}
}
}
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -1875,7 +1867,7 @@ impl Node<Literal> {
impl Literal { impl Literal {
pub fn new(value: LiteralValue) -> Node<Self> { pub fn new(value: LiteralValue) -> Node<Self> {
Node::no_src(Self { Node::no_src(Self {
raw: value.to_string(), raw: JValue::from(value.clone()).to_string(),
value, value,
digest: None, digest: None,
}) })
@ -1886,7 +1878,7 @@ impl From<Node<Literal>> for KclValue {
fn from(literal: Node<Literal>) -> Self { fn from(literal: Node<Literal>) -> Self {
let meta = vec![literal.metadata()]; let meta = vec![literal.metadata()];
match literal.inner.value { match literal.inner.value {
LiteralValue::Number { value, .. } => KclValue::Number { value, meta }, LiteralValue::Number(value) => KclValue::Number { value, meta },
LiteralValue::String(value) => KclValue::String { value, meta }, LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta }, LiteralValue::Bool(value) => KclValue::Bool { value, meta },
} }

View File

@ -126,13 +126,7 @@ impl From<BinaryOperator> for BinaryExpressionToken {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{ use crate::{parsing::ast::types::Literal, source_range::ModuleId};
parsing::{
ast::types::{Literal, LiteralValue},
token::NumericSuffix,
},
source_range::ModuleId,
};
#[test] #[test]
fn parse_and_evaluate() { fn parse_and_evaluate() {
@ -140,10 +134,7 @@ mod tests {
fn lit(n: u8) -> BinaryPart { fn lit(n: u8) -> BinaryPart {
BinaryPart::Literal(Box::new(Node::new( BinaryPart::Literal(Box::new(Node::new(
Literal { Literal {
value: LiteralValue::Number { value: n.into(),
value: n as f64,
suffix: NumericSuffix::None,
},
raw: n.to_string(), raw: n.to_string(),
digest: None, digest: None,
}, },

View File

@ -483,7 +483,7 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera
let (value, token) = any let (value, token) = any
.try_map(|token: Token| match token.token_type { .try_map(|token: Token| match token.token_type {
TokenType::Number => { TokenType::Number => {
let value: f64 = token.numeric_value().ok_or_else(|| { let x: f64 = token.numeric_value().ok_or_else(|| {
CompilationError::fatal(token.as_source_range(), format!("Invalid float: {}", token.value)) CompilationError::fatal(token.as_source_range(), format!("Invalid float: {}", token.value))
})?; })?;
@ -494,13 +494,7 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera
)); ));
} }
Ok(( Ok((LiteralValue::Number(x), token))
LiteralValue::Number {
value,
suffix: token.numeric_suffix(),
},
token,
))
} }
_ => Err(CompilationError::fatal(token.as_source_range(), "invalid literal")), _ => Err(CompilationError::fatal(token.as_source_range(), "invalid literal")),
}) })
@ -850,13 +844,13 @@ fn object_property(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
}; };
if sep.token_type == TokenType::Colon { if sep.token_type == TokenType::Colon {
ParseContext::warn( ParseContext::warn(CompilationError::with_suggestion(
CompilationError::err(
sep.into(), sep.into(),
Some(result.as_source_range()),
"Using `:` to initialize objects is deprecated, prefer using `=`.", "Using `:` to initialize objects is deprecated, prefer using `=`.",
) Some(("Replace `:` with `=`", " =")),
.with_suggestion("Replace `:` with `=`", " =", Tag::Deprecated), Tag::Deprecated,
); ));
} }
Ok(result) Ok(result)
@ -1075,20 +1069,10 @@ fn function_expr(i: &mut TokenSlice) -> PResult<Expr> {
let fn_tok = opt(fun).parse_next(i)?; let fn_tok = opt(fun).parse_next(i)?;
ignore_whitespace(i); ignore_whitespace(i);
let (result, has_arrow) = function_decl.parse_next(i)?; let (result, has_arrow) = function_decl.parse_next(i)?;
if fn_tok.is_none() { if fn_tok.is_none() && !has_arrow {
if has_arrow {
ParseContext::warn(
CompilationError::err(
result.as_source_range().start_as_range(),
"Missing `fn` in function declaration",
)
.with_suggestion("Add `fn`", "fn", Tag::None),
);
} else {
let err = CompilationError::fatal(result.as_source_range(), "Anonymous function requires `fn` before `(`"); let err = CompilationError::fatal(result.as_source_range(), "Anonymous function requires `fn` before `(`");
return Err(ErrMode::Cut(err.into())); return Err(ErrMode::Cut(err.into()));
} }
}
Ok(Expr::FunctionExpression(Box::new(result))) Ok(Expr::FunctionExpression(Box::new(result)))
} }
@ -1129,12 +1113,14 @@ fn function_decl(i: &mut TokenSlice) -> PResult<(Node<FunctionExpression>, bool)
open.module_id, open.module_id,
); );
let has_arrow = let has_arrow = if let Some(arrow) = arrow {
if let Some(arrow) = arrow { ParseContext::warn(CompilationError::with_suggestion(
ParseContext::warn( arrow.as_source_range(),
CompilationError::err(arrow.as_source_range(), "Unnecessary `=>` in function declaration") Some(result.as_source_range()),
.with_suggestion("Remove `=>`", "", Tag::Unnecessary), "Unnecessary `=>` in function declaration",
); Some(("Remove `=>`", "")),
Tag::Unnecessary,
));
true true
} else { } else {
false false
@ -1839,8 +1825,7 @@ fn declaration(i: &mut TokenSlice) -> PResult<BoxNode<VariableDeclaration>> {
ignore_whitespace(i); ignore_whitespace(i);
let val = let val = if kind == VariableKind::Fn {
if kind == VariableKind::Fn {
let eq = opt(equals).parse_next(i)?; let eq = opt(equals).parse_next(i)?;
ignore_whitespace(i); ignore_whitespace(i);
@ -1851,10 +1836,14 @@ fn declaration(i: &mut TokenSlice) -> PResult<BoxNode<VariableDeclaration>> {
.parse_next(i); .parse_next(i);
if let Some(t) = eq { if let Some(t) = eq {
ParseContext::warn( let ctxt_end = val.as_ref().map(|e| e.end()).unwrap_or(t.end);
CompilationError::err(t.as_source_range(), "Unnecessary `=` in function declaration") ParseContext::warn(CompilationError::with_suggestion(
.with_suggestion("Remove `=`", "", Tag::Unnecessary), t.as_source_range(),
); Some(SourceRange::new(id.start, ctxt_end, id.module_id)),
"Unnecessary `=` in function declaration",
Some(("Remove `=`", "")),
Tag::Unnecessary,
));
} }
val val
@ -1878,16 +1867,20 @@ fn declaration(i: &mut TokenSlice) -> PResult<BoxNode<VariableDeclaration>> {
.parse_next(i); .parse_next(i);
if let Some((_, tok)) = decl_token { if let Some((_, tok)) = decl_token {
ParseContext::warn( ParseContext::warn(CompilationError::with_suggestion(
CompilationError::err(
tok.as_source_range(), tok.as_source_range(),
Some(SourceRange::new(
id.start,
val.as_ref().map(|e| e.end()).unwrap_or(dec_end),
id.module_id,
)),
format!( format!(
"Using `{}` to declare constants is deprecated; no keyword is required", "Using `{}` to declare constants is deprecated; no keyword is required",
tok.value tok.value
), ),
) Some((format!("Remove `{}`", tok.value), "")),
.with_suggestion(format!("Remove `{}`", tok.value), "", Tag::Deprecated), Tag::Deprecated,
); ));
} }
val val
@ -2863,10 +2856,7 @@ mySk1 = startSketchAt([0, 0])"#;
ReturnStatement { ReturnStatement {
argument: Expr::Literal(Box::new(Node::new( argument: Expr::Literal(Box::new(Node::new(
Literal { Literal {
value: LiteralValue::Number { value: 2u32.into(),
value: 2.0,
suffix: NumericSuffix::None
},
raw: "2".to_owned(), raw: "2".to_owned(),
digest: None, digest: None,
}, },
@ -3067,15 +3057,7 @@ mySk1 = startSketchAt([0, 0])"#;
match &rhs.right { match &rhs.right {
BinaryPart::Literal(lit) => { BinaryPart::Literal(lit) => {
assert!(lit.start == 9 && lit.end == 10); assert!(lit.start == 9 && lit.end == 10);
assert!( assert!(lit.value == 3u32.into() && &lit.raw == "3" && lit.digest.is_none());
lit.value
== LiteralValue::Number {
value: 3.0,
suffix: NumericSuffix::None
}
&& &lit.raw == "3"
&& lit.digest.is_none()
);
} }
_ => panic!(), _ => panic!(),
} }
@ -3146,23 +3128,11 @@ mySk1 = startSketchAt([0, 0])"#;
let BinaryPart::Literal(left) = actual.inner.left else { let BinaryPart::Literal(left) = actual.inner.left else {
panic!("should be expression"); panic!("should be expression");
}; };
assert_eq!( assert_eq!(left.value, 1u32.into());
left.value,
LiteralValue::Number {
value: 1.0,
suffix: NumericSuffix::None
}
);
let BinaryPart::Literal(right) = actual.inner.right else { let BinaryPart::Literal(right) = actual.inner.right else {
panic!("should be expression"); panic!("should be expression");
}; };
assert_eq!( assert_eq!(right.value, 2u32.into());
right.value,
LiteralValue::Number {
value: 2.0,
suffix: NumericSuffix::None
}
);
} }
} }
@ -3479,10 +3449,7 @@ mySk1 = startSketchAt([0, 0])"#;
operator: BinaryOperator::Add, operator: BinaryOperator::Add,
left: BinaryPart::Literal(Box::new(Node::new( left: BinaryPart::Literal(Box::new(Node::new(
Literal { Literal {
value: LiteralValue::Number { value: 5u32.into(),
value: 5.0,
suffix: NumericSuffix::None,
},
raw: "5".to_owned(), raw: "5".to_owned(),
digest: None, digest: None,
}, },
@ -3531,10 +3498,7 @@ mySk1 = startSketchAt([0, 0])"#;
BinaryExpression { BinaryExpression {
left: BinaryPart::Literal(Box::new(Node::new( left: BinaryPart::Literal(Box::new(Node::new(
Literal { Literal {
value: LiteralValue::Number { value: 5u32.into(),
value: 5.0,
suffix: NumericSuffix::None,
},
raw: "5".to_string(), raw: "5".to_string(),
digest: None, digest: None,
}, },
@ -3545,10 +3509,7 @@ mySk1 = startSketchAt([0, 0])"#;
operator: BinaryOperator::Add, operator: BinaryOperator::Add,
right: BinaryPart::Literal(Box::new(Node::new( right: BinaryPart::Literal(Box::new(Node::new(
Literal { Literal {
value: LiteralValue::Number { value: 6u32.into(),
value: 6.0,
suffix: NumericSuffix::None,
},
raw: "6".to_string(), raw: "6".to_string(),
digest: None, digest: None,
}, },
@ -4384,20 +4345,6 @@ sketch001 = startSketchOn('XZ') |> startProfileAt([90.45, 119.09, %)"#;
return 0 return 0
}"# }"#
); );
let some_program_string = r#"myMap = map([0..5], (n) => {
return n * 2
})"#;
let (_, errs) = assert_no_err(some_program_string);
assert_eq!(errs.len(), 2);
let replaced = errs[0].apply_suggestion(some_program_string).unwrap();
let replaced = errs[1].apply_suggestion(&replaced).unwrap();
assert_eq!(
replaced,
r#"myMap = map([0..5], fn(n) {
return n * 2
})"#
);
} }
#[test] #[test]

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3851
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -19,10 +18,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 4, "start": 4,
"end": 5 "end": 5

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3852
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -19,10 +18,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 2, "start": 2,
"end": 3 "end": 3

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3853
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -19,10 +18,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 3, "start": 3,
"end": 4 "end": 4

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3854
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -23,10 +22,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 4, "start": 4,
"end": 5 "end": 5
@ -34,10 +30,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 3.0, "value": 3.0,
"suffix": "None"
},
"raw": "3", "raw": "3",
"start": 8, "start": 8,
"end": 9 "end": 9

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3855
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -23,10 +22,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 6, "start": 6,
"end": 7 "end": 7
@ -34,10 +30,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 3.0, "value": 3.0,
"suffix": "None"
},
"raw": "3", "raw": "3",
"start": 10, "start": 10,
"end": 11 "end": 11

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3856
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -12,10 +14,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -27,10 +26,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 6, "start": 6,
"end": 7 "end": 7
@ -38,10 +34,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 3.0, "value": 3.0,
"suffix": "None"
},
"raw": "3", "raw": "3",
"start": 10, "start": 10,
"end": 11 "end": 11
@ -55,10 +48,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 4.0, "value": 4.0,
"suffix": "None"
},
"raw": "4", "raw": "4",
"start": 16, "start": 16,
"end": 17 "end": 17

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3857
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -27,10 +26,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 6, "start": 6,
"end": 7 "end": 7
@ -38,10 +34,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 3.0, "value": 3.0,
"suffix": "None"
},
"raw": "3", "raw": "3",
"start": 10, "start": 10,
"end": 11 "end": 11
@ -52,10 +45,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 4.0, "value": 4.0,
"suffix": "None"
},
"raw": "4", "raw": "4",
"start": 16, "start": 16,
"end": 17 "end": 17

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3858
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -31,10 +30,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 7, "start": 7,
"end": 8 "end": 8
@ -42,10 +38,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 3.0, "value": 3.0,
"suffix": "None"
},
"raw": "3", "raw": "3",
"start": 11, "start": 11,
"end": 12 "end": 12
@ -56,10 +49,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 4.0, "value": 4.0,
"suffix": "None"
},
"raw": "4", "raw": "4",
"start": 17, "start": 17,
"end": 18 "end": 18
@ -70,10 +60,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 5.0, "value": 5.0,
"suffix": "None"
},
"raw": "5", "raw": "5",
"start": 21, "start": 21,
"end": 22 "end": 22

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3859
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 1.0, "value": 1.0,
"suffix": "None"
},
"raw": "1", "raw": "1",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -23,10 +22,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 8, "start": 8,
"end": 9 "end": 9
@ -34,10 +30,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 3.0, "value": 3.0,
"suffix": "None"
},
"raw": "3", "raw": "3",
"start": 12, "start": 12,
"end": 13 "end": 13

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3860
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -47,10 +49,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 6.0, "value": 6.0,
"suffix": "None"
},
"raw": "6", "raw": "6",
"start": 21, "start": 21,
"end": 22 "end": 22

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3861
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"type": "BinaryExpression", "type": "BinaryExpression",
@ -8,10 +10,7 @@ expression: actual
"left": { "left": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 2.0, "value": 2.0,
"suffix": "None"
},
"raw": "2", "raw": "2",
"start": 0, "start": 0,
"end": 1 "end": 1
@ -19,10 +18,7 @@ expression: actual
"right": { "right": {
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": {
"value": 3.0, "value": 3.0,
"suffix": "None"
},
"raw": "3", "raw": "3",
"start": 7, "start": 7,
"end": 8 "end": 8

View File

@ -25,10 +25,7 @@ expression: actual
"start": 27, "start": 27,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
}, },
{ {
"end": 31, "end": 31,
@ -36,10 +33,7 @@ expression: actual
"start": 30, "start": 30,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
} }
], ],
"end": 32, "end": 32,
@ -69,10 +63,7 @@ expression: actual
"start": 47, "start": 47,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
}, },
{ {
"end": 52, "end": 52,
@ -80,10 +71,7 @@ expression: actual
"start": 50, "start": 50,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 10.0
"value": 10.0,
"suffix": "None"
}
} }
], ],
"end": 53, "end": 53,
@ -120,10 +108,7 @@ expression: actual
"start": 81, "start": 81,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 5.0
"value": 5.0,
"suffix": "None"
}
}, },
"end": 82, "end": 82,
"operator": "-", "operator": "-",
@ -137,10 +122,7 @@ expression: actual
"start": 84, "start": 84,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 5.0
"value": 5.0,
"suffix": "None"
}
} }
], ],
"end": 86, "end": 86,
@ -176,10 +158,7 @@ expression: actual
"start": 104, "start": 104,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 5.0
"value": 5.0,
"suffix": "None"
}
}, },
{ {
"argument": { "argument": {
@ -188,10 +167,7 @@ expression: actual
"start": 108, "start": 108,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 15.0
"value": 15.0,
"suffix": "None"
}
}, },
"end": 110, "end": 110,
"operator": "-", "operator": "-",
@ -231,10 +207,7 @@ expression: actual
"start": 131, "start": 131,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 10.0
"value": 10.0,
"suffix": "None"
}
}, },
{ {
"end": 136, "end": 136,

View File

@ -1,6 +1,8 @@
--- ---
source: kcl/src/parsing/parser.rs source: kcl/src/parsing/parser.rs
assertion_line: 3964
expression: actual expression: actual
snapshot_kind: text
--- ---
{ {
"body": [ "body": [
@ -29,10 +31,7 @@ expression: actual
"start": 14, "start": 14,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
}, },
{ {
"argument": { "argument": {
@ -41,10 +40,7 @@ expression: actual
"start": 18, "start": 18,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
}, },
"end": 19, "end": 19,
"operator": "-", "operator": "-",

View File

@ -21,10 +21,7 @@ expression: actual
"start": 14, "start": 14,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 10.0
"value": 10.0,
"suffix": "None"
}
}, },
"endInclusive": true, "endInclusive": true,
"start": 10, "start": 10,
@ -34,10 +31,7 @@ expression: actual
"start": 11, "start": 11,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
}, },
"type": "ArrayRangeExpression", "type": "ArrayRangeExpression",
"type": "ArrayRangeExpression" "type": "ArrayRangeExpression"

View File

@ -23,10 +23,7 @@ expression: actual
"start": 50, "start": 50,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 2.0
"value": 2.0,
"suffix": "None"
}
}, },
"end": 51, "end": 51,
"start": 43, "start": 43,

View File

@ -25,10 +25,7 @@ expression: actual
"start": 26, "start": 26,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
}, },
{ {
"end": 29, "end": 29,
@ -36,10 +33,7 @@ expression: actual
"start": 28, "start": 28,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
} }
], ],
"end": 30, "end": 30,
@ -69,10 +63,7 @@ expression: actual
"start": 51, "start": 51,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
}, },
{ {
"end": 55, "end": 55,
@ -80,10 +71,7 @@ expression: actual
"start": 54, "start": 54,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
} }
], ],
"end": 56, "end": 56,
@ -126,10 +114,7 @@ expression: actual
"start": 89, "start": 89,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
}, },
{ {
"end": 93, "end": 93,
@ -137,10 +122,7 @@ expression: actual
"start": 92, "start": 92,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
} }
], ],
"end": 94, "end": 94,
@ -176,10 +158,7 @@ expression: actual
"start": 118, "start": 118,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
}, },
{ {
"end": 122, "end": 122,
@ -187,10 +166,7 @@ expression: actual
"start": 121, "start": 121,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
} }
], ],
"end": 123, "end": 123,

View File

@ -25,10 +25,7 @@ expression: actual
"start": 26, "start": 26,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
}, },
{ {
"end": 29, "end": 29,
@ -36,10 +33,7 @@ expression: actual
"start": 28, "start": 28,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 0.0
"value": 0.0,
"suffix": "None"
}
} }
], ],
"end": 30, "end": 30,
@ -69,10 +63,7 @@ expression: actual
"start": 43, "start": 43,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
}, },
{ {
"end": 47, "end": 47,
@ -80,10 +71,7 @@ expression: actual
"start": 46, "start": 46,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
} }
], ],
"end": 48, "end": 48,

View File

@ -23,10 +23,7 @@ expression: actual
"start": 10, "start": 10,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 1.0
"value": 1.0,
"suffix": "None"
}
} }
], ],
"callee": { "callee": {
@ -48,10 +45,7 @@ expression: actual
"start": 18, "start": 18,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": 2.0
"value": 2.0,
"suffix": "None"
}
}, },
{ {
"end": 22, "end": 22,

Some files were not shown because too many files have changed in this diff Show More