Compare commits

..

4 Commits

Author SHA1 Message Date
0b5cad701f fix import 2024-10-12 00:58:33 -04:00
71f7124913 Added tests 2024-10-11 13:45:43 -04:00
b6a3c552c9 Reload FileTree and File when changed externally 2024-10-11 13:45:43 -04:00
b303b678ad 2024-10-11 13:45:43 -04:00
148 changed files with 2054 additions and 5085 deletions

View File

@ -72,18 +72,6 @@ jobs:
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for nightly
if: ${{ github.event_name == 'schedule' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' }}
with:
name: prepared-files-nightly
path: |
electron-builder.yml
- id: export_notes
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
@ -93,7 +81,6 @@ jobs:
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: prepared-files-updater-test
path: |
@ -105,13 +92,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
platform: mac
- os: windows-2022
platform: win
- os: ubuntu-22.04
platform: linux
os: [macos-14, windows-2022, ubuntu-22.04]
runs-on: ${{ matrix.os }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
@ -140,16 +121,6 @@ jobs:
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
cp prepared-files/release-notes.md release-notes.md
- uses: actions/download-artifact@v3
if: ${{ github.event_name == 'schedule' }}
name: prepared-files-nightly
- name: Copy updated electron-builder.yml file for nightly build
if: ${{ github.event_name == 'schedule' }}
run: |
ls -R prepared-files-nightly
cp prepared-files-nightly/electron-builder.yml electron-builder.yml
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
@ -194,27 +165,9 @@ jobs:
- uses: actions/upload-artifact@v3
with:
name: out-arm64-${{ matrix.platform }}
# first two will pick both Zoo Modeling App-$VERSION-arm64-win.exe and Zoo Modeling App-$VERSION-win.exe
path: |
out/*-${{ env.VERSION_NO_V }}-win.*
out/*-${{ env.VERSION_NO_V }}-arm64-win.*
out/*-arm64-mac.*
out/*-arm64-linux.*
- uses: actions/upload-artifact@v3
with:
name: out-x64-${{ matrix.platform }}
path: |
out/*-x64-win.*
out/*-x64-mac.*
out/*-x86_64-linux.*
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: out-yml
name: out-${{ matrix.os }}
path: |
out/Zoo*.*
out/latest*.yml
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
@ -236,20 +189,10 @@ jobs:
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-arm64-${{ matrix.platform }}
name: updater-test-${{ matrix.os }}
path: |
out/*-arm64-win.exe
out/*-arm64-mac.dmg
out/*-arm64-linux.AppImage
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-x64-${{ matrix.platform }}
path: |
out/*-x64-win.exe
out/*-x64-mac.dmg
out/*-x86_64-linux.AppImage
out/Zoo*.*
out/latest*.yml
publish-apps-release:
@ -271,37 +214,17 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: out-arm64-win
name: out-windows-2022
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-win
name: out-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-mac
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-mac
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-linux
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-linux
path: out
- uses: actions/download-artifact@v3
with:
name: out-yml
name: out-ubuntu-22.04
path: out
- name: Generate the download static endpoint

View File

@ -37,6 +37,10 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
yarn-tsc:
@ -66,6 +70,10 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn lint
python-codespell:
@ -93,6 +101,11 @@ jobs:
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:bg

View File

@ -158,29 +158,11 @@ The PR may then serve as a place to discuss the human-readable changelog and ext
#### 3. Manually test artifacts from the Cut Release PR
##### Release builds
The release builds can be found under the `out-{platform}` zip, at the very bottom of the `build-publish-apps` summary page for each commit on this branch.
The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR.
##### Updater-test builds
The other `build-publish-apps` output in Cut Release PRs is `updater-test-{platform}`. As we don't have a way to test this fully automatically, we have a semi-automated process. For macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Modeling App\Zoo Modeling App.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Modeling App-{version}-{arch}-linux.AppImage
```
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
#### 4. Merge the Cut Release PR

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|> close(%)
|> patternCircular2d({
center: [0, 0],
instances: 13,
repetitions: 12,
arcDegrees: 360,
rotateDuplicates: true
}, %)

View File

@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|> patternCircular3d({
axis: [1, -1, 0],
center: [10, -20, 0],
instances: 11,
repetitions: 10,
arcDegrees: 360,
rotateDuplicates: true
}, %)

View File

@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|> circle({ center: [0, 0], radius: 1 }, %)
|> patternLinear2d({
axis: [1, 0],
instances: 7,
repetitions: 6,
distance: 4
}, %)

View File

@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
example = extrude(1, exampleSketch)
|> patternLinear3d({
axis: [1, 0, 1],
instances: 7,
repetitions: 6,
distance: 6
}, %)
```

View File

@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
fn decagon = (radius) => {
step = 1 / 10 * tau()
sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])
return reduce([1..10], sketch001, (i, sg) => {
return reduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], sketch001, (i, sg) => {
x = cos(step * i) * radius
y = sin(step * i) * radius
return lineTo([x, y], sg)

File diff suppressed because it is too large Load Diff

View File

@ -82,78 +82,6 @@ Raise a number to a power.
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -18,27 +18,6 @@ layout: manual
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
@ -66,7 +45,6 @@ layout: manual
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No |
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |

View File

@ -197,27 +197,6 @@ An expression can be evaluated to yield a single KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |

View File

@ -1,24 +0,0 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -1,16 +0,0 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number, number]`| The axis of the pattern. | No |

View File

@ -313,3 +313,45 @@ test(
await electronApp.close()
}
)
test(
'external change of file contents are reflected in editor',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const PROJECT_DIR_NAME = 'lee-was-here'
const {
electronApp,
page,
dir: projectsDir,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const aProjectDir = join(dir, PROJECT_DIR_NAME)
await fsp.mkdir(aProjectDir, { recursive: true })
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await test.step('Open the project', async () => {
await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible()
await page.getByText(PROJECT_DIR_NAME).click()
await u.waitForPageLoad()
})
await u.openFilePanel()
await u.openKclCodePanel()
await test.step('Write to file externally and check for changed content', async () => {
const content = 'ha he ho ho ha blap scap be dap'
await fsp.writeFile(
join(projectsDir, PROJECT_DIR_NAME, 'main.kcl'),
content
)
await u.editorTextMatches(content)
})
await electronApp.close()
}
)

View File

@ -960,4 +960,171 @@ _test.describe('Deleting items from the file pane', () => {
'TODO - delete folder we are in, with no main.kcl',
async () => {}
)
// Copied from tests above.
_test(
`external deletion of project navigates back home`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
// Point of divergence. Delete the project folder and see if it goes back
// to the home view.
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(join(projectsDirName, TEST_PROJECT_NAME), {
recursive: true,
force: true,
})
}
)
await _test.step('Check the app is back on the home view', async () => {
const projectsDirLink = page.getByText('Loaded from')
await _expect(projectsDirLink).toBeVisible()
})
await electronApp.close()
}
)
// Similar to the above
_test(
`external deletion of file in sub-directory updates the file tree and recreates it on code editor typing`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(
join(
projectsDirName,
TEST_PROJECT_NAME,
'folderToDelete',
'someFileWithin.kcl'
)
)
}
)
await _test.step('Check the file is gone in the file tree', async () => {
await _expect(
page.getByTestId('file-pane-scroll-container')
).not.toContainText('someFileWithin.kcl')
})
await _test.step(
'Check the file is back in the file tree after typing in code editor',
async () => {
await u.pasteCodeInEditor('hello = 1')
await _expect(
page.getByTestId('file-pane-scroll-container')
).toContainText('someFileWithin.kcl')
}
)
await electronApp.close()
}
)
})

View File

@ -669,7 +669,6 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
// exit sketch
@ -687,7 +686,6 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -9,7 +9,7 @@ import {
executorInputPath,
} from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
@ -445,6 +445,58 @@ test.describe('Testing settings', () => {
}
)
test(
'project settings reload on external change',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const {
electronApp,
page,
dir: projectDirName,
} = await setupElectron({
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
const logoLink = page.getByTestId('app-logo')
const projectDirLink = page.getByText('Loaded from')
await test.step('Wait for project view', async () => {
await expect(projectDirLink).toBeVisible()
})
const projectLinks = page.getByTestId('project-link')
const oldCount = await projectLinks.count()
await page.getByRole('button', { name: 'New project' }).click()
await expect(projectLinks).toHaveCount(oldCount + 1)
await projectLinks.filter({ hasText: 'project-000' }).first().click()
const changeColorFs = async (color: string) => {
const tempSettingsFilePath = join(
projectDirName,
'project-000',
PROJECT_SETTINGS_FILE_NAME
)
await fsp.writeFile(
tempSettingsFilePath,
`[settings.app]\nthemeColor = "${color}"`
)
}
await test.step('Check the color is first starting as we expect', async () => {
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
})
await test.step('Check color of logo changed', async () => {
await changeColorFs('99')
await expect(logoLink).toHaveCSS('--primary-hue', '99')
})
await electronApp.close()
}
)
test(
`Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' },

5
interface.d.ts vendored
View File

@ -20,9 +20,10 @@ export interface IElectronAPI {
version: typeof process.env.version
watchFileOn: (
path: string,
key: string,
callback: (eventType: string, path: string) => void
) => void
watchFileOff: (path: string) => void
watchFileOff: (path: string, key: string) => void
readFile: (path: string) => ReturnType<fs.readFile>
writeFile: (
path: string,
@ -73,7 +74,7 @@ export interface IElectronAPI {
callback: (value: { version: string }) => void
) => Electron.IpcRenderer
onUpdateDownloaded: (
callback: (value: { version: string; releaseNotes: string }) => void
callback: (value: string) => void
) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void

View File

@ -425,34 +425,6 @@
]
}
},
"/metrics": {
"get": {
"operationId": "get_metrics",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"title": "String",
"type": "string"
}
}
},
"description": "successful operation"
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
},
"summary": "List available machines and their statuses",
"tags": [
"hidden"
]
}
},
"/ping": {
"get": {
"operationId": "ping",
@ -520,13 +492,6 @@
}
},
"tags": [
{
"description": "Hidden API endpoints that should not show up in the docs.",
"externalDocs": {
"url": "https://docs.zoo.dev/api/machines"
},
"name": "hidden"
},
{
"description": "Utilities for making parts and discovering machines.",
"externalDocs": {

View File

@ -2,7 +2,7 @@ import type { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { Dispatch, useCallback, useEffect, useRef, useState } from 'react'
import { Dispatch, useCallback, useRef, useState } from 'react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -13,7 +13,6 @@ import { sortProject } from 'lib/desktopFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { useModelingContext } from 'hooks/useModelingContext'
@ -21,6 +20,8 @@ import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
@ -131,6 +132,23 @@ const FileTreeItem = ({
const isCurrentFile = fileOrDir.path === currentFile?.path
const itemRef = useRef(null)
// Since every file or directory gets its own FileTreeItem, we can do this.
// Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :)
useFileSystemWatcher(
async (eventType, path) => {
// Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') {
let code = await window.electron.readFile(path, 'utf-8')
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
}
fileSend({ type: 'Refresh' })
},
[fileOrDir.path]
)
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
const removeCurrentItemFromRenaming = useCallback(
() =>
@ -154,6 +172,13 @@ const FileTreeItem = ({
})
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
if (e.metaKey && e.key === 'Backspace') {
// Open confirmation dialog
@ -242,18 +267,8 @@ const FileTreeItem = ({
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => e.currentTarget.focus()}
onClickCapture={(e) =>
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
onFocusCapture={(e) =>
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
onClickCapture={clickDirectory}
onFocusCapture={clickDirectory}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
onKeyUp={handleKeyUp}
>
@ -469,27 +484,30 @@ export const FileTreeInner = ({
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { send: fileSend, context: fileContext } = useFileContext()
const { send: modelingSend } = useModelingContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when the document gets focus
useEffect(() => {
fileSend({ type: 'Refresh' })
}, [documentHasFocus])
// Refresh the file tree when there are changes.
useFileSystemWatcher(
async (eventType, path) => {
fileSend({ type: 'Refresh' })
},
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter(
(x: string | undefined) => x !== undefined
)
)
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}
return (
<div
className="overflow-auto pb-12 absolute inset-0"
data-testid="file-pane-scroll-container"
>
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}}
>
<ul className="m-0 p-0 text-sm" onClickCapture={clickDirectory}>
{sortProject(fileContext.project?.children || []).map((fileOrDir) => (
<FileTreeItem
project={fileContext.project}

View File

@ -221,6 +221,19 @@ export const SettingsAuthProviderBase = ({
useFileSystemWatcher(
async () => {
// If there is a projectPath but it no longer exists it means
// it was exterally removed. If we let the code past this condition
// execute it will recreate the directory due to code in
// loadAndValidateSettings trying to recreate files. I do not
// wish to change the behavior in case anything else uses it.
// Go home.
if (loadedProject?.project?.path) {
if (!window.electron.exists(loadedProject?.project?.path)) {
navigate(PATHS.HOME)
return
}
}
const data = await loadAndValidateSettings(loadedProject?.project?.path)
settingsSend({
type: 'Set all settings',
@ -228,7 +241,9 @@ export const SettingsAuthProviderBase = ({
doNotPersist: true,
})
},
settingsPath ? [settingsPath] : []
[settingsPath, loadedProject?.project?.path].filter(
(x: string | undefined) => x !== undefined
)
)
// Add settings commands to the command bar

View File

@ -1,153 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { ToastUpdate } from './ToastUpdate'
describe('ToastUpdate tests', () => {
const testData = {
version: '0.255.255',
files: [
{
url: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
size: 141277345,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.zip',
sha512:
'b+ugdg7A4LhYYJaFkPRxh1RvmGGMlPJJj7inkLg9PwRtCnR9ePMlktj2VRciXF1iLh59XW4bLc4dK1dFQHMULA==',
size: 135278259,
},
{
url: 'Zoo Modeling App-0.255.255-x64-mac.dmg',
sha512:
'gCUqww05yj8OYwPiTq6bo5GbkpngSbXGtenmDD7+kUm0UyVK8WD3dMAfQJtGNG5HY23aHCHe9myE2W4mbZGmiQ==',
size: 146004232,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.dmg',
sha512:
'ND871ayf81F1ZT+iWVLYTc2jdf/Py6KThuxX2QFWz14ebmIbJPL07lNtxQOexOFiuk0MwRhlCy1RzOSG1b9bmw==',
size: 140021522,
},
],
path: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
releaseNotes:
'## Some markdown release notes\n\n- This is a list item\n- This is another list item\n\n```javascript\nconsole.log("Hello, world!")\n```\n',
releaseDate: '2024-10-09T11:57:59.133Z',
} as const
test('Happy path: renders the toast with good data', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={testData.releaseNotes}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.getByTestId('release-notes')
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(restartButton).toBeEnabled()
fireEvent.click(restartButton)
expect(onRestart.mock.calls).toHaveLength(1)
expect(dismissButton).toBeEnabled()
fireEvent.click(dismissButton)
expect(onDismiss.mock.calls).toHaveLength(1)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(releaseNotes).toHaveTextContent('Release notes')
const releaseNotesListItems = screen.getAllByRole('listitem')
expect(releaseNotesListItems.map((el) => el.textContent)).toEqual([
'This is a list item',
'This is another list item',
])
})
test('Happy path: renders the breaking changes notice', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- This is a list item
- This is another list item with a breaking change
- This is a list item
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const releaseNotes = screen.getByText('Release notes', {
selector: 'summary',
})
const listItemContents = screen
.getAllByRole('listitem')
.map((el) => el.textContent)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(listItemContents).toEqual([
'This is a list item',
'This is another list item with a breaking change',
'This is a list item',
])
})
test('Missing release notes: renders the toast without release notes', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={''}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.queryByText(/release notes/i, {
selector: 'details > summary',
})
const releaseNotesListItem = screen.queryByRole('listitem', {
name: /this is a list item/i,
})
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(releaseNotes).not.toBeInTheDocument()
expect(releaseNotesListItem).not.toBeInTheDocument()
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
})

View File

@ -1,23 +1,14 @@
import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
export function ToastUpdate({
version,
releaseNotes,
onRestart,
onDismiss,
}: {
version: string
releaseNotes?: string
onRestart: () => void
onDismiss: () => void
}) {
const containsBreakingChanges = releaseNotes
?.toLocaleLowerCase()
.includes('breaking')
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -28,7 +19,7 @@ export function ToastUpdate({
>
v{version}
</span>
<p className="ml-4 text-md text-bold">
<span className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '}
<a
@ -41,39 +32,15 @@ export function ToastUpdate({
>
here on GitHub.
</a>
</p>
</span>
</div>
{releaseNotes && (
<details
className="my-4 border border-chalkboard-30 dark:border-chalkboard-60 rounded"
open={containsBreakingChanges}
data-testid="release-notes"
>
<summary className="p-2 select-none cursor-pointer">
Release notes
{containsBreakingChanges && (
<strong className="text-destroy-50"> (Breaking changes)</strong>
)}
</summary>
<div
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
gfm: true,
breaks: true,
sanitize: true,
}),
}}
></div>
</details>
)}
<div className="flex justify-between gap-8">
<ActionButton
Element="button"
iconStart={{
icon: 'arrowRotateRight',
}}
name="restart"
name="Restart app now"
onClick={onRestart}
>
Restart app now
@ -83,10 +50,9 @@ export function ToastUpdate({
iconStart={{
icon: 'checkmark',
}}
name="dismiss"
name="Got it"
onClick={() => {
toast.dismiss()
onDismiss()
}}
>
Got it

View File

@ -1,9 +1,6 @@
import { styleTags, tags as t } from '@lezer/highlight'
export const kclHighlight = styleTags({
'import export': t.moduleKeyword,
ImportItemAs: t.definitionKeyword,
ImportFrom: t.moduleKeyword,
'fn var let const': t.definitionKeyword,
'if else': t.controlKeyword,
return: t.controlKeyword,

View File

@ -15,9 +15,8 @@
}
statement[@isGroup=Statement] {
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression }
}
@ -26,9 +25,6 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
Body { "{" statement* "}" }
ImportItems { commaSep1NoTrailingComma<ImportItem> }
ImportItem { identifier (ImportItemAs identifier)? }
expression[@isGroup=Expression] {
String |
Number |
@ -78,8 +74,6 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
commaSep<term> { (term ("," term)*)? ","? }
commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
@ -112,9 +106,6 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
Shebang { "#!" ![\n]* }
ImportItemAs { "as" }
ImportFrom { "from" }
"(" ")"
"{" "}"
"[" "]"

View File

@ -12,35 +12,51 @@ type Path = string
// watcher.addListener(() => { ... }).
export const useFileSystemWatcher = (
callback: (path: Path) => Promise<void>,
dependencyArray: Path[]
callback: (eventType: string, path: Path) => Promise<void>,
paths: Path[]
): void => {
// Track a ref to the callback. This is how we get the callback updated
// across the NodeJS<->Browser boundary.
const callbackRef = useRef<{ fn: (path: Path) => Promise<void> }>({
fn: async (_path) => {},
})
// Used to track this instance of useFileSystemWatcher.
// Assign to ref so it doesn't change between renders.
const key = useRef(Math.random().toString())
const [output, setOutput] = useState<
{ eventType: string; path: string } | undefined
>(undefined)
// Used to track if paths list changes.
const [pathsTracked, setPathsTracked] = useState<Path[]>([])
useEffect(() => {
callbackRef.current.fn = callback
}, [callback])
// Used to track if dependencyArrray changes.
const [dependencyArrayTracked, setDependencyArrayTracked] = useState<Path[]>(
[]
)
if (!output) return
callback(output.eventType, output.path).catch(reportRejection)
}, [output])
// On component teardown obliterate all watchers.
useEffect(() => {
// The hook is useless on web.
if (!isDesktop()) return
const cbWatcher = (eventType: string, path: string) => {
setOutput({ eventType, path })
}
for (let path of pathsTracked) {
// Because functions don't retain refs between NodeJS-Browser I need to
// pass an identifying key so we can later remove it.
// A way to think of the function call is:
// "For this path, add a new handler with this key"
// "There can be many keys (functions) per path"
// Again if refs were preserved, we wouldn't need to do this. Keys
// gives us uniqueness.
window.electron.watchFileOn(path, key.current, cbWatcher)
}
return () => {
for (let path of dependencyArray) {
window.electron.watchFileOff(path)
for (let path of pathsTracked) {
window.electron.watchFileOff(path, key.current)
}
}
}, [])
}, [pathsTracked])
function difference<T>(l1: T[], l2: T[]): [T[], T[]] {
return [
@ -49,8 +65,7 @@ export const useFileSystemWatcher = (
]
}
const hasDiff =
difference(dependencyArray, dependencyArrayTracked)[0].length !== 0
const hasDiff = difference(paths, pathsTracked)[0].length !== 0
// Removing 1 watcher at a time is only possible because in a filesystem,
// a path is unique (there can never be two paths with the same name).
@ -61,19 +76,8 @@ export const useFileSystemWatcher = (
if (!hasDiff) return
const [pathsRemoved, pathsRemaining] = difference(
dependencyArrayTracked,
dependencyArray
)
for (let path of pathsRemoved) {
window.electron.watchFileOff(path)
}
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
for (let path of pathsAdded) {
window.electron.watchFileOn(path, (_eventType: string, path: Path) => {
callbackRef.current.fn(path).catch(reportRejection)
})
}
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
const [, pathsRemaining] = difference(pathsTracked, paths)
const [pathsAdded] = difference(paths, pathsTracked)
setPathsTracked(pathsRemaining.concat(pathsAdded))
}, [hasDiff])
}

View File

@ -293,24 +293,6 @@ code {
which lets you use them with @apply in your CSS, and get
autocomplete in classNames in your JSX.
*/
.parsed-markdown ul,
.parsed-markdown ol {
@apply list-outside pl-4 lg:pl-8 my-2;
}
.parsed-markdown ul li {
@apply list-disc;
}
.parsed-markdown li p {
@apply inline;
}
.parsed-markdown code {
@apply px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 text-chalkboard-80;
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
}
}
#code-mirror-override .cm-scroller,

View File

@ -70,17 +70,15 @@ if (isDesktop()) {
id: AUTO_UPDATER_TOAST_ID,
})
})
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => {
window.electron.onUpdateDownloaded((version: string) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message)
toast.custom(
ToastUpdate({
version,
releaseNotes,
onRestart: () => {
window.electron.appRestart()
},
onDismiss: () => {},
}),
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
)

View File

@ -501,7 +501,6 @@ export function sketchOnExtrudedFace(
createIdentifier(extrudeName ? extrudeName : oldSketchName),
_tag,
]),
undefined,
'const'
)
@ -683,7 +682,6 @@ export function createPipeExpression(
export function createVariableDeclaration(
varName: string,
init: VariableDeclarator['init'],
visibility: VariableDeclaration['visibility'] = 'default',
kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration {
return {
@ -701,7 +699,6 @@ export function createVariableDeclaration(
init,
},
],
visibility,
kind,
}
}

View File

@ -620,7 +620,7 @@ describe('Testing button states', () => {
it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
})
it('should return false when body exists and not a segment is selected', async () => {
it('hould return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false)
})
})

View File

@ -1,7 +1,5 @@
import {
CallExpression,
Expr,
Identifier,
ObjectExpression,
PathToNode,
Program,
@ -29,7 +27,7 @@ import {
sketchLineHelperMap,
} from '../std/sketch'
import { err, trap } from 'lib/trap'
import { Selections } from 'lib/selections'
import { Selections, canFilletSelection } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import {
ArtifactGraph,
@ -68,10 +66,7 @@ export function modifyAstCloneWithFilletAndTag(
const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<
PathToNode,
Array<{ tag: string; selectionType: string }>
> = new Map()
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selectionRange of selection.codeBasedSelections) {
@ -79,7 +74,6 @@ export function modifyAstCloneWithFilletAndTag(
codeBasedSelections: [selectionRange],
otherSelections: [],
}
const selectionType = singleSelection.codeBasedSelections[0].type
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
@ -95,7 +89,6 @@ export function modifyAstCloneWithFilletAndTag(
)
if (err(tagResult)) return tagResult
const { tag } = tagResult
const tagInfo = { tag, selectionType }
// Group tags by their corresponding extrude node
const extrudeKey = JSON.stringify(pathToExtrudeNode)
@ -103,29 +96,23 @@ export function modifyAstCloneWithFilletAndTag(
if (lookupMap.has(extrudeKey)) {
const existingPath = lookupMap.get(extrudeKey)
if (!existingPath) return new Error('Path to extrude node not found.')
extrudeToTagsMap.get(existingPath)?.push(tagInfo)
extrudeToTagsMap.get(existingPath)?.push(tag)
} else {
lookupMap.set(extrudeKey, pathToExtrudeNode)
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
}
}
// Step 2: Apply fillet(s) for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags
const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const tagCalls = tagInfos.map(({ tag, selectionType }) => {
return getEdgeTagCall(tag, selectionType)
})
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radiusValue,
tags: createArrayExpression(tagCalls),
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
}),
createPipeSubstitution(),
])
@ -157,7 +144,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
firstTag
tags[0]
)
pathToFilletNodes.push(pathToFilletNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
@ -178,7 +165,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
firstTag
tags[0]
)
pathToFilletNodes.push(pathToFilletNode)
} else {
@ -289,21 +276,6 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag }
}
function getEdgeTagCall(
tag: string,
selectionType: string
): Identifier | CallExpression {
let tagCall: Expr = createIdentifier(tag)
// Modify the tag based on selectionType
if (selectionType === 'edge') {
tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall])
} else if (selectionType === 'adjacent-edge') {
tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall])
}
return tagCall
}
function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
@ -339,7 +311,7 @@ function locateExtrudeDeclarator(
function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: Identifier | CallExpression
tag: string
): PathToNode {
let pathToFilletObj: PathToNode = []
let inFillet = false
@ -375,30 +347,12 @@ function getPathToNodeOfFilletLiteral(
]
}
function hasTag(
node: ObjectExpression,
tag: Identifier | CallExpression
): boolean {
function hasTag(node: ObjectExpression, tag: string): boolean {
return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
// if selection is a base edge:
if (tag.type === 'Identifier') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag.name
)
}
// if selection is an adjacent or opposite edge:
if (tag.type === 'CallExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'CallExpression' &&
element.callee.name === tag.callee.name && // edge location
element.arguments[0].type === 'Identifier' &&
tag.arguments[0].type === 'Identifier' &&
element.arguments[0].name === tag.arguments[0].name // tag name
)
}
return prop.value.elements.some(
(element) => element.type === 'Identifier' && element.name === tag
)
}
return false
})
@ -429,7 +383,7 @@ export const hasValidFilletSelection = ({
ast: Program
code: string
}) => {
// check if there is anything filletable in the scene
// case 0: check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
@ -440,88 +394,65 @@ export const hasValidFilletSelection = ({
})
if (!extrudeExists) return false
// check if nothing is selected
if (selectionRanges.codeBasedSelections.length === 0) {
return true
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
}
// check if selection is last string in code
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
return true
}
// selection exists:
for (const selection of selectionRanges.codeBasedSelections) {
// check if all selections are in sketchLineHelperMap
const path = getNodePathFromSourceRange(ast, selection.range)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
path,
'CallExpression'
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
)
if (err(segmentNode)) return false
if (segmentNode.node.type !== 'CallExpression') {
return false
}
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
return false
}
// check if selection is extruded
// TODO: option 1 : extrude is in the sketch pipe
// option 2: extrude is outside the sketch pipe
const extrudeExists = hasSketchPipeBeenExtruded(selection, ast)
if (err(extrudeExists)) {
return false
}
if (!extrudeExists) {
return false
}
// check if tag exists for the selection
let tagExists = false
let tag = ''
traverse(segmentNode.node, {
enter(node) {
if (node.type === 'TagDeclarator') {
tagExists = true
tag = node.value
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
// Add check whether the tag exists at all:
if (!(segmentNode.node.arguments.length === 3)) return true
// If the tag exists, check if it is already filleted
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
},
})
// check if tag is used in fillet
if (tagExists) {
// create tag call
let tagCall: Expr = getEdgeTagCall(tag, selection.type)
// check if tag is used in fillet
let inFillet = false
let tagUsedInFillet = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (hasTag(node, tagCall)) {
tagUsedInFillet = true
}
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
if (tagUsedInFillet) {
return false
}
} else {
return false
}
}
return true
return canFilletSelection(selectionRanges)
}
type EdgeTypes =

View File

@ -28,7 +28,6 @@ import {
getConstraintType,
} from './std/sketchcombos'
import { err } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -121,12 +120,7 @@ export function getNodeFromPathCurry(
}
function moreNodePathFromSourceRange(
node:
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']]
): PathToNode {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 577 KiB

View File

@ -28,8 +28,9 @@ class FileSystemManager {
)
}
return this.join(this.dir, path).then((filePath) => {
return window.electron.readFile(filePath)
return this.join(this.dir, path).then(async (filePath) => {
const content = await window.electron.readFile(filePath)
return content
})
}

View File

@ -426,7 +426,6 @@ export const _executor = async (
baseUnit,
engineCommandManager,
fileSystemManager,
undefined,
isMock
)
return execStateFromRaw(execState)

3
src/lib/codeEditor.ts Normal file
View File

@ -0,0 +1,3 @@
export const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -448,7 +448,7 @@ export const readProjectSettingsFile = async (
}
}
const configToml = await window.electron.readFile(settingsPath)
const configToml = await window.electron.readFile(settingsPath, 'utf-8')
const configObj = parseProjectSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
@ -467,7 +467,7 @@ export const readAppSettingsFile = async () => {
// The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath)
const configToml = await window.electron.readFile(settingsPath, 'utf-8')
const parsedAppConfig = parseAppSettings(configToml)
if (err(parsedAppConfig)) {
return Promise.reject(parsedAppConfig)
@ -527,7 +527,7 @@ export const readTokenFile = async () => {
let settingsPath = await getTokenFilePath()
if (window.electron.exists(settingsPath)) {
const token: string = await window.electron.readFile(settingsPath)
const token: string = await window.electron.readFile(settingsPath, 'utf-8')
if (!token) return ''
return token

View File

@ -55,23 +55,6 @@ export interface paths {
patch?: never
trace?: never
}
'/metrics': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** List available machines and their statuses */
get: operations['get_metrics']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/ping': {
parameters: {
query?: never
@ -295,28 +278,6 @@ export interface operations {
'5XX': components['responses']['Error']
}
}
get_metrics: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody?: never
responses: {
/** @description successful operation */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': string
}
}
'4XX': components['responses']['Error']
'5XX': components['responses']['Error']
}
}
ping: {
parameters: {
query?: never

View File

@ -14,6 +14,7 @@ import { codeManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { getProjectInfo } from './desktop'
import { createSettings } from './settings/initialSettings'
import { normalizeLineEndings } from 'lib/codeEditor'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
@ -108,7 +109,7 @@ export const fileLoader: LoaderFunction = async (
)
}
code = await window.electron.readFile(currentFilePath)
code = await window.electron.readFile(currentFilePath, 'utf-8')
code = normalizeLineEndings(code)
// Update both the state and the editor's code.
@ -182,7 +183,3 @@ export const homeLoader: LoaderFunction = async (): Promise<
}
return {}
}
const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -37,8 +37,6 @@ if (!process.env.NODE_ENV)
// dotenv override when present
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
console.log(process.env)
process.env.VITE_KC_API_WS_MODELING_URL ??=
'wss://api.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
@ -287,10 +285,7 @@ app.on('ready', () => {
autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info)
mainWindow?.webContents.send('update-downloaded', {
version: info.version,
releaseNotes: info.releaseNotes,
})
mainWindow?.webContents.send('update-downloaded', info.version)
})
ipcMain.handle('app.restart', () => {

View File

@ -16,12 +16,11 @@ const startDeviceFlow = (host: string): Promise<string> =>
ipcRenderer.invoke('startDeviceFlow', host)
const loginWithDeviceFlow = (): Promise<string> =>
ipcRenderer.invoke('loginWithDeviceFlow')
const onUpdateDownloaded = (
callback: (value: { version: string; releaseNotes: string }) => void
) => ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateDownloadStart = (
callback: (value: { version: string }) => void
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
const onUpdateDownloaded = (callback: (value: string) => void) =>
ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateError = (callback: (value: Error) => void) =>
ipcRenderer.on('update-error', (_event, value) => callback(value))
const appRestart = () => ipcRenderer.invoke('app.restart')
@ -30,22 +29,51 @@ const isMac = os.platform() === 'darwin'
const isWindows = os.platform() === 'win32'
const isLinux = os.platform() === 'linux'
let fsWatchListeners = new Map<string, ReturnType<typeof chokidar.watch>>()
let fsWatchListeners = new Map<
string,
Map<
string,
{
watcher: ReturnType<typeof chokidar.watch>
callback: (eventType: string, path: string) => void
}
>
>()
const watchFileOn = (path: string, callback: (path: string) => void) => {
const watcherMaybe = fsWatchListeners.get(path)
if (watcherMaybe) return
const watcher = chokidar.watch(path)
const watchFileOn = (
path: string,
key: string,
callback: (eventType: string, path: string) => void
) => {
let watchers = fsWatchListeners.get(path)
if (!watchers) {
watchers = new Map()
}
const watcher = chokidar.watch(path, { depth: 1 })
watcher.on('all', callback)
fsWatchListeners.set(path, watcher)
watchers.set(key, { watcher, callback })
fsWatchListeners.set(path, watchers)
}
const watchFileOff = (path: string) => {
const watcher = fsWatchListeners.get(path)
if (!watcher) return
watcher.unwatch(path)
fsWatchListeners.delete(path)
const watchFileOff = (path: string, key: string) => {
const watchers = fsWatchListeners.get(path)
if (!watchers) return
const data = watchers.get(key)
if (!data) {
console.warn(
"Trying to remove a watcher, callback that doesn't exist anymore. Suspicious."
)
return
}
const { watcher, callback } = data
watcher.off('all', callback)
watchers.delete(key)
if (watchers.size === 0) {
fsWatchListeners.delete(path)
} else {
fsWatchListeners.set(path, watchers)
}
}
const readFile = (path: string) => fs.readFile(path, 'utf-8')
const readFile = (path: string, as?: string) => fs.readFile(path, as)
// It seems like from the node source code this does not actually block but also
// don't trust me on that (jess).
const exists = (path: string) => fsSync.existsSync(path)

View File

@ -1394,9 +1394,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.3"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97eb9a8e0cd5b76afea91d7eecd5cf8338cd44ced04256cf1f800474b227c52"
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
dependencies = [
"bytemuck",
"byteorder-lite",
@ -1533,16 +1533,16 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.72"
version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kcl-lib"
version = "0.2.22"
version = "0.2.20"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1617,7 +1617,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.14"
version = "0.1.12"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -2337,18 +2337,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.88"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.22.5"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51"
checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
dependencies = [
"cfg-if",
"indoc",
@ -2364,9 +2364,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.22.5"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179"
checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
dependencies = [
"once_cell",
"target-lexicon",
@ -2374,9 +2374,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.22.5"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d"
checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
dependencies = [
"libc",
"pyo3-build-config",
@ -2384,9 +2384,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.22.5"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e"
checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@ -2396,9 +2396,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.22.5"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce"
checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -3829,9 +3829,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom",
"serde",
@ -3907,9 +3907,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.95"
version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887"
dependencies = [
"cfg-if",
"once_cell",
@ -3918,9 +3918,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.95"
version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e"
dependencies = [
"bumpalo",
"log",
@ -3946,9 +3946,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.95"
version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -3956,9 +3956,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.95"
version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6"
dependencies = [
"proc-macro2",
"quote",
@ -3969,9 +3969,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.95"
version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9"
[[package]]
name = "wasm-lib"
@ -4032,9 +4032,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.72"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@ -18,31 +18,31 @@ kittycad.workspace = true
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
[dev-dependencies]
anyhow = "1"
image = { version = "0.25.3", default-features = false, features = ["png"] }
image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true }
kittycad-modeling-cmds = { workspace = true }
pretty_assertions = "1.4.1"
reqwest = { version = "0.12", default-features = false }
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
futures = "0.3.31"
js-sys = "0.3.72"
js-sys = "0.3.71"
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream"] }
wasm-streams = "0.4.1"
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3.72"
version = "0.3.69"
features = [
"console",
"HtmlTextAreaElement",

View File

@ -762,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -51,7 +51,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -18,7 +18,7 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -52,7 +52,7 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -18,7 +18,7 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -52,7 +52,7 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -51,7 +51,7 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_some_function {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, None, id_generator).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -1,7 +1,7 @@
extern crate alloc;
use kcl_lib::ast::types::{
BodyItem, Expr, Identifier, ItemVisibility, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration,
VariableDeclarator, VariableKind,
BodyItem, Expr, Identifier, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration, VariableDeclarator,
VariableKind,
};
use kcl_macros::parse;
use pretty_assertions::assert_eq;
@ -33,7 +33,6 @@ fn basic() {
})),
digest: None,
}],
visibility: ItemVisibility::Default,
kind: VariableKind::Const,
digest: None,
})],

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.14"
version = "0.1.12"
edition = "2021"
license = "MIT"

View File

@ -178,7 +178,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
// Let users know if the test is taking a long time.
let (done_tx, done_rx) = oneshot::channel::<()>();
let timer = time_until(done_rx);
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator, None).await {
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator).await {
Ok(sn) => sn,
Err(e) => return kcl_err(e),
};

View File

@ -20,4 +20,4 @@ kcl-lib = { path = "../kcl" }
kittycad = { workspace = true, features = ["clap"] }
kittycad-modeling-cmds = { workspace = true }
tokio = { version = "1.38", features = ["full", "time", "rt", "tracing"] }
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
uuid = { version = "1.9.1", features = ["v4", "js", "serde"] }

View File

@ -1,7 +1,6 @@
use anyhow::Result;
use indexmap::IndexMap;
use kcl_lib::{
engine::ExecutionKind,
errors::KclError,
executor::{DefaultPlanes, IdGenerator},
};
@ -27,7 +26,6 @@ pub struct EngineConnection {
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
core_test: Arc<Mutex<String>>,
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
impl EngineConnection {
@ -41,7 +39,6 @@ impl EngineConnection {
batch_end: Arc::new(Mutex::new(IndexMap::new())),
core_test: result,
default_planes: Default::default(),
execution_kind: Default::default(),
})
}
@ -363,18 +360,6 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
id_generator: &mut IdGenerator,

View File

@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
settings: Default::default(),
context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
};
let _memory = ctx.run(&program, None, IdGenerator::default(), None).await?;
let _memory = ctx.run(&program, None, IdGenerator::default()).await?;
let result = result.lock().expect("mutex lock").clone();
Ok(result)

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.22"
version = "0.2.20"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -26,7 +26,7 @@ futures = { version = "0.3.31" }
git_rev = "0.1.0"
gltf-json = "1.4.1"
http = { workspace = true }
image = { version = "0.25.3", default-features = false, features = ["png"] }
image = { version = "0.25.1", default-features = false, features = ["png"] }
indexmap = { version = "2.6.0", features = ["serde"] }
kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
@ -34,7 +34,7 @@ lazy_static = "1.5.0"
measurements = "0.11.0"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.5", optional = true }
pyo3 = { version = "0.22.3", optional = true }
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1", "preserve_order"] }
@ -47,18 +47,18 @@ toml = "0.8.19"
ts-rs = { version = "10.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.6.18"
zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.72" }
js-sys = { version = "0.3.71" }
tokio = { version = "1.40.0", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
web-sys = { version = "0.3.72", features = ["console"] }
web-sys = { version = "0.3.69", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"
@ -93,7 +93,7 @@ criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0"
handlebars = "6.1.0"
iai = "0.1"
image = { version = "0.25.3", default-features = false, features = ["png"] }
image = { version = "0.25.1", default-features = false, features = ["png"] }
insta = { version = "1.40.0", features = ["json"] }
itertools = "0.13.0"
pretty_assertions = "1.4.1"

View File

@ -48,10 +48,7 @@ pub async fn modify_ast_for_sketch(
// Get the information about the sketch.
if let Some(ast_sketch) = program.get_variable(sketch_name) {
let constraint_level = match ast_sketch {
super::types::Definition::Variable(var) => var.get_constraint_level(),
super::types::Definition::Import(import) => import.get_constraint_level(),
};
let constraint_level = ast_sketch.get_constraint_level();
match &constraint_level {
ConstraintLevel::None { source_ranges: _ } => {}
ConstraintLevel::Ignore { source_ranges: _ } => {}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,13 @@
use crate::errors::KclError;
use crate::executor::BodyType;
use crate::executor::ExecState;
use crate::executor::ExecutorContext;
use crate::executor::KclValue;
use crate::executor::Metadata;
use crate::executor::SourceRange;
use crate::executor::StatementKind;
use super::compute_digest;
use super::impl_value_meta;
use super::ConstraintLevel;
use super::Hover;
@ -8,6 +15,7 @@ use super::{Digest, Expr};
use databake::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest as DigestTrait, Sha256};
// TODO: This should be its own type, similar to Program,
// but guaranteed to have an Expression as its final item.
@ -48,6 +56,14 @@ impl_value_meta!(IfExpression);
impl_value_meta!(ElseIf);
impl IfExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
for else_if in &mut slf.else_ifs {
hasher.update(else_if.compute_digest());
}
hasher.update(slf.final_else.compute_digest());
});
fn source_ranges(&self) -> Vec<SourceRange> {
vec![SourceRange::from(self)]
}
@ -85,12 +101,63 @@ impl From<&ElseIf> for Metadata {
}
impl ElseIf {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
});
#[allow(dead_code)]
fn source_ranges(&self) -> Vec<SourceRange> {
vec![SourceRange([self.start, self.end])]
}
}
// Execution
impl IfExpression {
#[async_recursion::async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
// Check the `if` branch.
let cond = ctx
.execute_expr(&self.cond, exec_state, &Metadata::from(self), StatementKind::Expression)
.await?
.get_bool()?;
if cond {
let block_result = ctx.inner_execute(&self.then_val, exec_state, BodyType::Block).await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
// Check any `else if` branches.
for else_if in &self.else_ifs {
let cond = ctx
.execute_expr(
&else_if.cond,
exec_state,
&Metadata::from(self),
StatementKind::Expression,
)
.await?
.get_bool()?;
if cond {
let block_result = ctx
.inner_execute(&else_if.then_val, exec_state, BodyType::Block)
.await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
}
// Run the final `else` branch.
ctx.inner_execute(&self.final_else, exec_state, BodyType::Block)
.await
.map(|expr| expr.unwrap())
}
}
// IDE support and refactors
impl IfExpression {
@ -141,3 +208,8 @@ impl ElseIf {
self.then_val.rename_identifiers(old_name, new_name);
}
}
// Linting
impl IfExpression {}
impl ElseIf {}

View File

@ -1,391 +0,0 @@
use sha2::{Digest as DigestTrait, Sha256};
use super::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ElseIf, Expr,
ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression, ImportItem, ImportStatement, Literal,
LiteralIdentifier, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression,
ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator,
UnaryExpression, VariableDeclaration, VariableDeclarator,
};
/// Position-independent digest of the AST node.
pub type Digest = [u8; 32];
macro_rules! compute_digest {
(|$slf:ident, $hasher:ident| $body:block) => {
/// Compute a digest over the AST node.
pub fn compute_digest(&mut self) -> Digest {
if let Some(node_digest) = self.digest {
return node_digest;
}
let mut $hasher = Sha256::new();
#[allow(unused_mut)]
let mut $slf = self;
$hasher.update(std::any::type_name::<Self>());
$body
let node_digest: Digest = $hasher.finalize().into();
$slf.digest = Some(node_digest);
node_digest
}
};
}
impl ImportItem {
compute_digest!(|slf, hasher| {
let name = slf.name.name.as_bytes();
hasher.update(name.len().to_ne_bytes());
hasher.update(name);
if let Some(alias) = &mut slf.alias {
hasher.update([1]);
hasher.update(alias.compute_digest());
} else {
hasher.update([0]);
}
});
}
impl ImportStatement {
compute_digest!(|slf, hasher| {
for item in &mut slf.items {
hasher.update(item.compute_digest());
}
let path = slf.path.as_bytes();
hasher.update(path.len().to_ne_bytes());
hasher.update(path);
});
}
impl Program {
compute_digest!(|slf, hasher| {
hasher.update(slf.body.len().to_ne_bytes());
for body_item in slf.body.iter_mut() {
hasher.update(body_item.compute_digest());
}
hasher.update(slf.non_code_meta.compute_digest());
});
}
impl BodyItem {
pub fn compute_digest(&mut self) -> Digest {
match self {
BodyItem::ImportStatement(s) => s.compute_digest(),
BodyItem::ExpressionStatement(es) => es.compute_digest(),
BodyItem::VariableDeclaration(vs) => vs.compute_digest(),
BodyItem::ReturnStatement(rs) => rs.compute_digest(),
}
}
}
impl Expr {
pub fn compute_digest(&mut self) -> Digest {
match self {
Expr::Literal(lit) => lit.compute_digest(),
Expr::Identifier(id) => id.compute_digest(),
Expr::TagDeclarator(tag) => tag.compute_digest(),
Expr::BinaryExpression(be) => be.compute_digest(),
Expr::FunctionExpression(fe) => fe.compute_digest(),
Expr::CallExpression(ce) => ce.compute_digest(),
Expr::PipeExpression(pe) => pe.compute_digest(),
Expr::PipeSubstitution(ps) => ps.compute_digest(),
Expr::ArrayExpression(ae) => ae.compute_digest(),
Expr::ArrayRangeExpression(are) => are.compute_digest(),
Expr::ObjectExpression(oe) => oe.compute_digest(),
Expr::MemberExpression(me) => me.compute_digest(),
Expr::UnaryExpression(ue) => ue.compute_digest(),
Expr::IfExpression(e) => e.compute_digest(),
Expr::None(_) => {
let mut hasher = Sha256::new();
hasher.update(b"Value::None");
hasher.finalize().into()
}
}
}
}
impl BinaryPart {
pub fn compute_digest(&mut self) -> Digest {
match self {
BinaryPart::Literal(lit) => lit.compute_digest(),
BinaryPart::Identifier(id) => id.compute_digest(),
BinaryPart::BinaryExpression(be) => be.compute_digest(),
BinaryPart::CallExpression(ce) => ce.compute_digest(),
BinaryPart::UnaryExpression(ue) => ue.compute_digest(),
BinaryPart::MemberExpression(me) => me.compute_digest(),
BinaryPart::IfExpression(e) => e.compute_digest(),
}
}
}
impl MemberObject {
pub fn compute_digest(&mut self) -> Digest {
match self {
MemberObject::MemberExpression(me) => me.compute_digest(),
MemberObject::Identifier(id) => id.compute_digest(),
}
}
}
impl LiteralIdentifier {
pub fn compute_digest(&mut self) -> Digest {
match self {
LiteralIdentifier::Identifier(id) => id.compute_digest(),
LiteralIdentifier::Literal(lit) => lit.compute_digest(),
}
}
}
impl FnArgType {
pub fn compute_digest(&mut self) -> Digest {
let mut hasher = Sha256::new();
match self {
FnArgType::Primitive(prim) => {
hasher.update(b"FnArgType::Primitive");
hasher.update(prim.digestable_id())
}
FnArgType::Array(prim) => {
hasher.update(b"FnArgType::Array");
hasher.update(prim.digestable_id())
}
FnArgType::Object { properties } => {
hasher.update(b"FnArgType::Object");
hasher.update(properties.len().to_ne_bytes());
for prop in properties.iter_mut() {
hasher.update(prop.compute_digest());
}
}
}
hasher.finalize().into()
}
}
impl Parameter {
compute_digest!(|slf, hasher| {
hasher.update(slf.identifier.compute_digest());
match &mut slf.type_ {
Some(arg) => {
hasher.update(b"Parameter::type_::Some");
hasher.update(arg.compute_digest())
}
None => {
hasher.update(b"Parameter::type_::None");
}
}
hasher.update(if slf.optional { [1] } else { [0] })
});
}
impl FunctionExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.params.len().to_ne_bytes());
for param in slf.params.iter_mut() {
hasher.update(param.compute_digest());
}
hasher.update(slf.body.compute_digest());
match &mut slf.return_type {
Some(rt) => {
hasher.update(b"FunctionExpression::return_type::Some");
hasher.update(rt.compute_digest());
}
None => {
hasher.update(b"FunctionExpression::return_type::None");
}
}
});
}
impl ReturnStatement {
compute_digest!(|slf, hasher| {
hasher.update(slf.argument.compute_digest());
});
}
impl NonCodeNode {
compute_digest!(|slf, hasher| {
match &slf.value {
NonCodeValue::Shebang { value } => {
hasher.update(value);
}
NonCodeValue::InlineComment { value, style } => {
hasher.update(value);
hasher.update(style.digestable_id());
}
NonCodeValue::BlockComment { value, style } => {
hasher.update(value);
hasher.update(style.digestable_id());
}
NonCodeValue::NewLineBlockComment { value, style } => {
hasher.update(value);
hasher.update(style.digestable_id());
}
NonCodeValue::NewLine => {
hasher.update(b"\r\n");
}
}
});
}
impl NonCodeMeta {
compute_digest!(|slf, hasher| {
let mut keys = slf.non_code_nodes.keys().copied().collect::<Vec<_>>();
keys.sort();
for key in keys.into_iter() {
hasher.update(key.to_ne_bytes());
let nodes = slf.non_code_nodes.get_mut(&key).unwrap();
hasher.update(nodes.len().to_ne_bytes());
for node in nodes.iter_mut() {
hasher.update(node.compute_digest());
}
}
});
}
impl ExpressionStatement {
compute_digest!(|slf, hasher| {
hasher.update(slf.expression.compute_digest());
});
}
impl VariableDeclaration {
compute_digest!(|slf, hasher| {
hasher.update(slf.declarations.len().to_ne_bytes());
for declarator in &mut slf.declarations {
hasher.update(declarator.compute_digest());
}
hasher.update(slf.visibility.digestable_id());
hasher.update(slf.kind.digestable_id());
});
}
impl VariableDeclarator {
compute_digest!(|slf, hasher| {
hasher.update(slf.id.compute_digest());
hasher.update(slf.init.compute_digest());
});
}
impl Literal {
compute_digest!(|slf, hasher| {
hasher.update(slf.value.digestable_id());
});
}
impl Identifier {
compute_digest!(|slf, hasher| {
let name = slf.name.as_bytes();
hasher.update(name.len().to_ne_bytes());
hasher.update(name);
});
}
impl TagDeclarator {
compute_digest!(|slf, hasher| {
let name = slf.name.as_bytes();
hasher.update(name.len().to_ne_bytes());
hasher.update(name);
});
}
impl PipeSubstitution {
compute_digest!(|slf, hasher| {
hasher.update(b"PipeSubstitution");
});
}
impl ArrayExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.elements.len().to_ne_bytes());
for value in slf.elements.iter_mut() {
hasher.update(value.compute_digest());
}
});
}
impl ArrayRangeExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.start_element.compute_digest());
hasher.update(slf.end_element.compute_digest());
});
}
impl ObjectExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.properties.len().to_ne_bytes());
for prop in slf.properties.iter_mut() {
hasher.update(prop.compute_digest());
}
});
}
impl ObjectProperty {
compute_digest!(|slf, hasher| {
hasher.update(slf.key.compute_digest());
hasher.update(slf.value.compute_digest());
});
}
impl MemberExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.object.compute_digest());
hasher.update(slf.property.compute_digest());
hasher.update(if slf.computed { [1] } else { [0] });
});
}
impl BinaryExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.operator.digestable_id());
hasher.update(slf.left.compute_digest());
hasher.update(slf.right.compute_digest());
});
}
impl UnaryExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.operator.digestable_id());
hasher.update(slf.argument.compute_digest());
});
}
impl PipeExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.body.len().to_ne_bytes());
for value in slf.body.iter_mut() {
hasher.update(value.compute_digest());
}
hasher.update(slf.non_code_meta.compute_digest());
});
}
impl CallExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.callee.compute_digest());
hasher.update(slf.arguments.len().to_ne_bytes());
for argument in slf.arguments.iter_mut() {
hasher.update(argument.compute_digest());
}
hasher.update(if slf.optional { [1] } else { [0] });
});
}
impl IfExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
for else_if in &mut slf.else_ifs {
hasher.update(else_if.compute_digest());
}
hasher.update(slf.final_else.compute_digest());
});
}
impl ElseIf {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
});
}

View File

@ -1,785 +0,0 @@
use super::{
human_friendly_type, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart,
CallExpression, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject,
ObjectExpression, TagDeclarator, UnaryExpression, UnaryOperator,
};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{
BodyType, ExecState, ExecutorContext, KclValue, Metadata, Sketch, SourceRange, StatementKind, TagEngineInfo,
TagIdentifier, UserVal,
},
std::FunctionKind,
};
use async_recursion::async_recursion;
use serde_json::Value as JValue;
impl BinaryPart {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
match self {
BinaryPart::Literal(literal) => Ok(literal.into()),
BinaryPart::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
Ok(value.clone())
}
BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
BinaryPart::CallExpression(call_expression) => call_expression.execute(exec_state, ctx).await,
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state),
BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
}
}
}
impl MemberExpression {
pub fn get_result_array(&self, exec_state: &mut ExecState, index: usize) -> Result<KclValue, KclError> {
let array = match &self.object {
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone()
}
};
let array_json = array.get_json_value()?;
if let serde_json::Value::Array(array) = array_json {
if let Some(value) = array.get(index) {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("index {} not found in array", index),
source_ranges: vec![self.clone().into()],
}))
}
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression array is not an array: {:?}", array),
source_ranges: vec![self.clone().into()],
}))
}
}
pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
#[derive(Debug)]
enum Property {
Number(usize),
String(String),
}
impl Property {
fn type_name(&self) -> &'static str {
match self {
Property::Number(_) => "number",
Property::String(_) => "string",
}
}
}
let property_src: SourceRange = self.property.clone().into();
let property_sr = vec![property_src];
let property: Property = match self.property.clone() {
LiteralIdentifier::Identifier(identifier) => {
let name = identifier.name;
if !self.computed {
// Treat the property as a literal
Property::String(name.to_string())
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(&name, property_src)?;
let KclValue::UserVal(prop) = prop else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
),
}));
};
match prop.value {
JValue::Number(ref num) => {
num
.as_u64()
.and_then(|x| usize::try_from(x).ok())
.map(Property::Number)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name}'s value is not a valid property/index, you can only use a string or int (>= 0) here",
),
})
})?
}
JValue::String(ref x) => Property::String(x.to_owned()),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array",
),
}));
}
}
}
}
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
match value {
LiteralValue::IInteger(x) => {
if let Ok(x) = u64::try_from(x) {
Property::Number(x.try_into().unwrap())
} else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
}));
}
}
LiteralValue::String(s) => Property::String(s),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
}));
}
}
}
};
let object = match &self.object {
// TODO: Don't use recursion here, use a loop.
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone()
}
};
let object_json = object.get_json_value()?;
// Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object_json, property) {
(JValue::Object(map), Property::String(property)) => {
if let Some(value) = map.get(&property) {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property '{property}' not found in object"),
source_ranges: vec![self.clone().into()],
}))
}
}
(JValue::Object(_), p) => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only strings can be used as the property of an object, but you're using a {}",
p.type_name()
),
source_ranges: vec![self.clone().into()],
})),
(JValue::Array(arr), Property::Number(index)) => {
let value_of_arr: Option<&JValue> = arr.get(index);
if let Some(value) = value_of_arr {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("The array doesn't have any item at index {index}"),
source_ranges: vec![self.clone().into()],
}))
}
}
(JValue::Array(_), p) => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only integers >= 0 can be used as the index of an array, but you're using a {}",
p.type_name()
),
source_ranges: vec![self.clone().into()],
})),
(being_indexed, _) => {
let t = human_friendly_type(&being_indexed);
Err(KclError::Semantic(KclErrorDetails {
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"),
source_ranges: vec![self.clone().into()],
}))
}
}
}
}
impl BinaryExpression {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let left_json_value = self.left.get_result(exec_state, ctx).await?.get_json_value()?;
let right_json_value = self.right.get_result(exec_state, ctx).await?.get_json_value()?;
// First check if we are doing string concatenation.
if self.operator == BinaryOperator::Add {
if let (Some(left), Some(right)) = (
parse_json_value_as_string(&left_json_value),
parse_json_value_as_string(&right_json_value),
) {
let value = serde_json::Value::String(format!("{}{}", left, right));
return Ok(KclValue::UserVal(UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
}
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?;
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?;
let value: serde_json::Value = match self.operator {
BinaryOperator::Add => (left + right).into(),
BinaryOperator::Sub => (left - right).into(),
BinaryOperator::Mul => (left * right).into(),
BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::Eq => (left == right).into(),
BinaryOperator::Neq => (left != right).into(),
BinaryOperator::Gt => (left > right).into(),
BinaryOperator::Gte => (left >= right).into(),
BinaryOperator::Lt => (left < right).into(),
BinaryOperator::Lte => (left <= right).into(),
};
Ok(KclValue::UserVal(UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
impl UnaryExpression {
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
if self.operator == UnaryOperator::Not {
let value = self.argument.get_result(exec_state, ctx).await?.get_json_value()?;
let Some(bool_value) = json_as_bool(&value) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot apply unary operator ! to non-boolean value: {}", value),
source_ranges: vec![self.into()],
}));
};
let negated = !bool_value;
return Ok(KclValue::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64(
&self.argument.get_result(exec_state, ctx).await?.get_json_value()?,
self.into(),
)?;
Ok(KclValue::UserVal(UserVal {
value: (-(num)).into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
pub(crate) async fn execute_pipe_body(
exec_state: &mut ExecState,
body: &[Expr],
source_range: SourceRange,
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
let Some((first, body)) = body.split_first() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Pipe expressions cannot be empty".to_owned(),
source_ranges: vec![source_range],
}));
};
// Evaluate the first element in the pipeline.
// They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
// they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value
// of its own.
let meta = Metadata {
source_range: SourceRange([first.start(), first.end()]),
};
let output = ctx
.execute_expr(first, exec_state, &meta, StatementKind::Expression)
.await?;
// Now that we've evaluated the first child expression in the pipeline, following child expressions
// should use the previous child expression for %.
// This means there's no more need for the previous pipe_value from the parent AST node above this one.
let previous_pipe_value = std::mem::replace(&mut exec_state.pipe_value, Some(output));
// Evaluate remaining elements.
let result = inner_execute_pipe_body(exec_state, body, ctx).await;
// Restore the previous pipe value.
exec_state.pipe_value = previous_pipe_value;
result
}
/// Execute the tail of a pipe expression. exec_state.pipe_value must be set by
/// the caller.
#[async_recursion]
async fn inner_execute_pipe_body(
exec_state: &mut ExecState,
body: &[Expr],
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
for expression in body {
match expression {
Expr::TagDeclarator(_) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("This cannot be in a PipeExpression: {:?}", expression),
source_ranges: vec![expression.into()],
}));
}
Expr::Literal(_)
| Expr::Identifier(_)
| Expr::BinaryExpression(_)
| Expr::FunctionExpression(_)
| Expr::CallExpression(_)
| Expr::PipeExpression(_)
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::IfExpression(_)
| Expr::None(_) => {}
};
let metadata = Metadata {
source_range: SourceRange([expression.start(), expression.end()]),
};
let output = ctx
.execute_expr(expression, exec_state, &metadata, StatementKind::Expression)
.await?;
exec_state.pipe_value = Some(output);
}
// Safe to unwrap here, because pipe_value always has something pushed in when the `match first` executes.
let final_output = exec_state.pipe_value.take().unwrap();
Ok(final_output)
}
impl CallExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let fn_name = &self.callee.name;
let mut fn_args: Vec<KclValue> = Vec::with_capacity(self.arguments.len());
for arg in &self.arguments {
let metadata = Metadata {
source_range: SourceRange::from(arg),
};
let result = ctx
.execute_expr(arg, exec_state, &metadata, StatementKind::Expression)
.await?;
fn_args.push(result);
}
match ctx.stdlib.get_either(&self.callee.name) {
FunctionKind::Core(func) => {
// Attempt to call the function.
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let mut result = func.std_lib_fn()(exec_state, args).await?;
// If the return result is a sketch or solid, we want to update the
// memory for the tags of the group.
// TODO: This could probably be done in a better way, but as of now this was my only idea
// and it works.
match result {
KclValue::UserVal(ref mut uval) => {
uval.mutate(|sketch: &mut Sketch| {
for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag(&tag.value, tag.clone())?;
}
Ok::<_, KclError>(())
})?;
}
KclValue::Solid(ref mut solid) => {
for value in &solid.value {
if let Some(tag) = value.get_tag() {
// Get the past tag and update it.
let mut t = if let Some(t) = solid.sketch.tags.get(&tag.name) {
t.clone()
} else {
// It's probably a fillet or a chamfer.
// Initialize it.
TagIdentifier {
value: tag.name.clone(),
info: Some(TagEngineInfo {
id: value.get_id(),
surface: Some(value.clone()),
path: None,
sketch: solid.id,
}),
meta: vec![Metadata {
source_range: tag.clone().into(),
}],
}
};
let Some(ref info) = t.info else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Tag {} does not have path info", tag.name),
source_ranges: vec![tag.into()],
}));
};
let mut info = info.clone();
info.surface = Some(value.clone());
info.sketch = solid.id;
t.info = Some(info);
exec_state.memory.update_tag(&tag.name, t.clone())?;
// update the sketch tags.
solid.sketch.tags.insert(tag.name.clone(), t);
}
}
// Find the stale sketch in memory and update it.
if let Some(current_env) = exec_state
.memory
.environments
.get_mut(exec_state.memory.current_env.index())
{
current_env.update_sketch_tags(&solid.sketch);
}
}
_ => {}
}
Ok(result)
}
FunctionKind::Std(func) => {
let function_expression = func.function();
let (required_params, optional_params) =
function_expression.required_and_optional_params().map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: format!("Error getting parts of function: {}", e),
source_ranges: vec![self.into()],
})
})?;
if fn_args.len() < required_params.len() || fn_args.len() > function_expression.params.len() {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"this function expected {} arguments, got {}",
required_params.len(),
fn_args.len(),
),
source_ranges: vec![self.into()],
}));
}
// Add the arguments to the memory.
let mut fn_memory = exec_state.memory.clone();
for (index, param) in required_params.iter().enumerate() {
fn_memory.add(
&param.identifier.name,
fn_args.get(index).unwrap().clone(),
param.identifier.clone().into(),
)?;
}
// Add the optional arguments to the memory.
for (index, param) in optional_params.iter().enumerate() {
if let Some(arg) = fn_args.get(index + required_params.len()) {
fn_memory.add(&param.identifier.name, arg.clone(), param.identifier.clone().into())?;
} else {
fn_memory.add(
&param.identifier.name,
KclValue::UserVal(UserVal {
value: serde_json::value::Value::Null,
meta: Default::default(),
}),
param.identifier.clone().into(),
)?;
}
}
let fn_dynamic_state = exec_state.dynamic_state.clone();
// TODO: Shouldn't we merge program memory into fn_dynamic_state
// here?
// Call the stdlib function
let p = &func.function().body;
let (exec_result, fn_memory) = {
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = ctx.inner_execute(p, exec_state, BodyType::Block).await;
exec_state.dynamic_state = previous_dynamic_state;
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
(result, fn_memory)
};
match exec_result {
Ok(_) => {}
Err(err) => {
// We need to override the source ranges so we don't get the embedded kcl
// function from the stdlib.
return Err(err.override_source_ranges(vec![self.into()]));
}
};
let out = fn_memory.return_;
let result = out.ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of stdlib function {} is undefined", fn_name),
source_ranges: vec![self.into()],
})
})?;
Ok(result)
}
FunctionKind::UserDefined => {
let source_range = SourceRange::from(self);
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = exec_state.memory.get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
let return_value = {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = func.call_fn(fn_args, exec_state, ctx.clone()).await.map_err(|e| {
// Add the call expression to the source ranges.
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;
result?
};
let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![source_range];
// We want to send the source range of the original function.
if let KclValue::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect();
};
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
})
})?;
Ok(result)
}
}
}
}
impl TagDeclarator {
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
value: self.name.clone(),
info: None,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
exec_state.memory.add(&self.name, memory_item.clone(), self.into())?;
Ok(self.into())
}
}
impl ArrayExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let mut results = Vec::with_capacity(self.elements.len());
for element in &self.elements {
let metadata = Metadata::from(element);
// TODO: Carry statement kind here so that we know if we're
// inside a variable declaration.
let value = ctx
.execute_expr(element, exec_state, &metadata, StatementKind::Expression)
.await?;
results.push(value.get_json_value()?);
}
Ok(KclValue::UserVal(UserVal {
value: results.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
impl ArrayRangeExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let metadata = Metadata::from(&*self.start_element);
let start = ctx
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let start = parse_json_number_as_u64(&start, (&*self.start_element).into())?;
let metadata = Metadata::from(&*self.end_element);
let end = ctx
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let end = parse_json_number_as_u64(&end, (&*self.end_element).into())?;
if end < start {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Range start is greater than range end: {start} .. {end}"),
}));
}
let range: Vec<_> = if self.end_inclusive {
(start..=end).map(JValue::from).collect()
} else {
(start..end).map(JValue::from).collect()
};
Ok(KclValue::UserVal(UserVal {
value: range.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
impl ObjectExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let mut object = serde_json::Map::new();
for property in &self.properties {
let metadata = Metadata::from(&property.value);
let result = ctx
.execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression)
.await?;
object.insert(property.key.name.clone(), result.get_json_value()?);
}
Ok(KclValue::UserVal(UserVal {
value: object.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
pub fn parse_json_number_as_u64(j: &serde_json::Value, source_range: SourceRange) -> Result<u64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_u64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
})
})
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
}))
}
}
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_f64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
})
})
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
}))
}
}
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(n) = &j {
Some(n.clone())
} else {
None
}
}
/// JSON value as bool. If it isn't a bool, returns None.
pub fn json_as_bool(j: &serde_json::Value) -> Option<bool> {
match j {
JValue::Null => None,
JValue::Bool(b) => Some(*b),
JValue::Number(_) => None,
JValue::String(_) => None,
JValue::Array(_) => None,
JValue::Object(_) => None,
}
}
impl IfExpression {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
// Check the `if` branch.
let cond = ctx
.execute_expr(&self.cond, exec_state, &Metadata::from(self), StatementKind::Expression)
.await?
.get_bool()?;
if cond {
let block_result = ctx.inner_execute(&self.then_val, exec_state, BodyType::Block).await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
// Check any `else if` branches.
for else_if in &self.else_ifs {
let cond = ctx
.execute_expr(
&else_if.cond,
exec_state,
&Metadata::from(self),
StatementKind::Expression,
)
.await?
.get_bool()?;
if cond {
let block_result = ctx
.inner_execute(&else_if.then_val, exec_state, BodyType::Block)
.await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
}
// Run the final `else` branch.
ctx.inner_execute(&self.final_else, exec_state, BodyType::Block)
.await
.map(|expr| expr.unwrap())
}
}

View File

@ -784,9 +784,6 @@ fn test_generate_stdlib_markdown_docs() {
#[test]
fn test_generate_stdlib_json_schema() {
// If this test fails and you've modified the AST or something else which affects the json repr
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
// test data, then check `/docs/kcl/std.json` to ensure the changes are expected.
let stdlib = StdLib::new();
let combined = stdlib.combined();

View File

@ -859,7 +859,7 @@ mod tests {
assert_eq!(
snippet,
r#"patternCircular3d({
instances: ${0:10},
repetitions: ${0:10},
axis: [${1:3.14}, ${2:3.14}, ${3:3.14}],
center: [${4:3.14}, ${5:3.14}, ${6:3.14}],
arcDegrees: ${7:3.14},
@ -921,7 +921,7 @@ mod tests {
assert_eq!(
snippet,
r#"patternLinear2d({
instances: ${0:10},
repetitions: ${0:10},
distance: ${1:3.14},
axis: [${2:3.14}, ${3:3.14}],
}, ${4:%})${}"#

View File

@ -24,8 +24,6 @@ use crate::{
executor::{DefaultPlanes, IdGenerator},
};
use super::ExecutionKind;
#[derive(Debug, PartialEq)]
enum SocketHealth {
Active,
@ -48,8 +46,6 @@ pub struct EngineConnection {
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
/// If the server sends session data, it'll be copied to here.
session_data: Arc<Mutex<Option<ModelingSessionData>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
pub struct TcpRead {
@ -304,7 +300,6 @@ impl EngineConnection {
batch_end: Arc::new(Mutex::new(IndexMap::new())),
default_planes: Default::default(),
session_data,
execution_kind: Default::default(),
})
}
}
@ -319,18 +314,6 @@ impl EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
id_generator: &mut IdGenerator,

View File

@ -22,13 +22,10 @@ use crate::{
executor::{DefaultPlanes, IdGenerator},
};
use super::ExecutionKind;
#[derive(Debug, Clone)]
pub struct EngineConnection {
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
impl EngineConnection {
@ -36,7 +33,6 @@ impl EngineConnection {
Ok(EngineConnection {
batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())),
execution_kind: Default::default(),
})
}
}
@ -51,18 +47,6 @@ impl crate::engine::EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
_id_generator: &mut IdGenerator,

View File

@ -9,7 +9,6 @@ use kittycad_modeling_cmds as kcmc;
use wasm_bindgen::prelude::*;
use crate::{
engine::ExecutionKind,
errors::{KclError, KclErrorDetails},
executor::{DefaultPlanes, IdGenerator},
};
@ -43,7 +42,6 @@ pub struct EngineConnection {
manager: Arc<EngineCommandManager>,
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
// Safety: WebAssembly will only ever run in a single-threaded context.
@ -56,7 +54,6 @@ impl EngineConnection {
manager: Arc::new(manager),
batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())),
execution_kind: Default::default(),
})
}
}
@ -71,18 +68,6 @@ impl crate::engine::EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
_id_generator: &mut IdGenerator,

View File

@ -41,23 +41,6 @@ lazy_static::lazy_static! {
pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
}
/// The mode of execution. When isolated, like during an import, attempting to
/// send a command results in an error.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum ExecutionKind {
#[default]
Normal,
Isolated,
}
impl ExecutionKind {
pub fn is_isolated(&self) -> bool {
matches!(self, ExecutionKind::Isolated)
}
}
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of commands to be sent to the engine.
@ -66,13 +49,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of end commands to be sent to the engine.
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>;
/// Get the current execution kind.
fn execution_kind(&self) -> ExecutionKind;
/// Replace the current execution kind with a new value and return the
/// existing value.
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
/// Get the default planes.
async fn default_planes(
&self,
@ -126,10 +102,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
source_range: crate::executor::SourceRange,
cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let execution_kind = self.execution_kind();
if execution_kind.is_isolated() {
return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] }));
}
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id.into(),

View File

@ -14,8 +14,6 @@ pub enum KclError {
Syntax(KclErrorDetails),
#[error("semantic: {0:?}")]
Semantic(KclErrorDetails),
#[error("import cycle: {0:?}")]
ImportCycle(KclErrorDetails),
#[error("type: {0:?}")]
Type(KclErrorDetails),
#[error("unimplemented: {0:?}")]
@ -54,7 +52,6 @@ impl KclError {
KclError::Lexical(_) => "lexical",
KclError::Syntax(_) => "syntax",
KclError::Semantic(_) => "semantic",
KclError::ImportCycle(_) => "import cycle",
KclError::Type(_) => "type",
KclError::Unimplemented(_) => "unimplemented",
KclError::Unexpected(_) => "unexpected",
@ -71,7 +68,6 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges.clone(),
KclError::Syntax(e) => e.source_ranges.clone(),
KclError::Semantic(e) => e.source_ranges.clone(),
KclError::ImportCycle(e) => e.source_ranges.clone(),
KclError::Type(e) => e.source_ranges.clone(),
KclError::Unimplemented(e) => e.source_ranges.clone(),
KclError::Unexpected(e) => e.source_ranges.clone(),
@ -89,7 +85,6 @@ impl KclError {
KclError::Lexical(e) => &e.message,
KclError::Syntax(e) => &e.message,
KclError::Semantic(e) => &e.message,
KclError::ImportCycle(e) => &e.message,
KclError::Type(e) => &e.message,
KclError::Unimplemented(e) => &e.message,
KclError::Unexpected(e) => &e.message,
@ -107,7 +102,6 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges = source_ranges,
KclError::Syntax(e) => e.source_ranges = source_ranges,
KclError::Semantic(e) => e.source_ranges = source_ranges,
KclError::ImportCycle(e) => e.source_ranges = source_ranges,
KclError::Type(e) => e.source_ranges = source_ranges,
KclError::Unimplemented(e) => e.source_ranges = source_ranges,
KclError::Unexpected(e) => e.source_ranges = source_ranges,
@ -127,7 +121,6 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
KclError::Type(e) => e.source_ranges.extend(source_ranges),
KclError::Unimplemented(e) => e.source_ranges.extend(source_ranges),
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),

View File

@ -1,9 +1,6 @@
//! The executor for the AST.
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use std::{collections::HashMap, sync::Arc};
use anyhow::Result;
use async_recursion::async_recursion;
@ -26,12 +23,12 @@ type Point3D = kcmc::shared::Point3d<f64>;
use crate::{
ast::types::{
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, ImportStatement, ItemVisibility,
KclNone, Program, ReturnStatement, TagDeclarator,
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, KclNone, Program,
ReturnStatement, TagDeclarator,
},
engine::{EngineManager, ExecutionKind},
engine::EngineManager,
errors::{KclError, KclErrorDetails},
fs::{FileManager, FileSystem},
fs::FileManager,
settings::types::UnitLength,
std::{FnAsArg, StdLib},
};
@ -50,14 +47,6 @@ pub struct ExecState {
/// The current value of the pipe operator returned from the previous
/// expression. If we're not currently in a pipeline, this will be None.
pub pipe_value: Option<KclValue>,
/// Identifiers that have been exported from the current module.
pub module_exports: HashSet<String>,
/// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<std::path::PathBuf>,
/// The directory of the current project. This is used for resolving import
/// paths. If None is given, the current working directory is used.
pub project_directory: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -402,20 +391,6 @@ impl KclValue {
KclValue::Face(_) => "Face",
}
}
pub(crate) fn is_function(&self) -> bool {
match self {
KclValue::UserVal(..)
| KclValue::TagIdentifier(..)
| KclValue::TagDeclarator(..)
| KclValue::Plane(..)
| KclValue::Face(..)
| KclValue::Solid(..)
| KclValue::Solids { .. }
| KclValue::ImportedGeometry(..) => false,
KclValue::Function { .. } => true,
}
}
}
impl From<SketchSet> for KclValue {
@ -477,15 +452,6 @@ pub enum Geometries {
Solids(Vec<Box<Solid>>),
}
impl From<Geometry> for Geometries {
fn from(value: Geometry) -> Self {
match value {
Geometry::Sketch(x) => Self::Sketches(vec![x]),
Geometry::Solid(x) => Self::Solids(vec![x]),
}
}
}
/// A sketch or a group of sketches.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -1529,14 +1495,6 @@ impl From<SourceRange> for Metadata {
}
}
impl From<&ImportStatement> for Metadata {
fn from(stmt: &ImportStatement) -> Self {
Self {
source_range: SourceRange::new(stmt.start, stmt.end),
}
}
}
impl From<&ExpressionStatement> for Metadata {
fn from(exp_statement: &ExpressionStatement) -> Self {
Self {
@ -2000,9 +1958,8 @@ impl ExecutorContext {
program: &crate::ast::types::Program,
memory: Option<ProgramMemory>,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<ExecState, KclError> {
self.run_with_session_data(program, memory, id_generator, project_directory)
self.run_with_session_data(program, memory, id_generator)
.await
.map(|x| x.0)
}
@ -2014,7 +1971,6 @@ impl ExecutorContext {
program: &crate::ast::types::Program,
memory: Option<ProgramMemory>,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<(ExecState, Option<ModelingSessionData>), KclError> {
let memory = if let Some(memory) = memory {
memory.clone()
@ -2024,7 +1980,6 @@ impl ExecutorContext {
let mut exec_state = ExecState {
memory,
id_generator,
project_directory,
..Default::default()
};
// Before we even start executing the program, set the units.
@ -2063,91 +2018,6 @@ impl ExecutorContext {
// Iterate over the body of the program.
for statement in &program.body {
match statement {
BodyItem::ImportStatement(import_stmt) => {
let source_range = SourceRange::from(import_stmt);
let path = import_stmt.path.clone();
let resolved_path = if let Some(project_dir) = &exec_state.project_directory {
std::path::PathBuf::from(project_dir).join(&path)
} else {
std::path::PathBuf::from(&path)
};
if exec_state.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
resolved_path.to_string_lossy()
),
source_ranges: vec![import_stmt.into()],
}));
}
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
let program = crate::parser::parse(&source)?;
let (module_memory, module_exports) = {
exec_state.import_stack.push(resolved_path.clone());
let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated);
let original_memory = std::mem::take(&mut exec_state.memory);
let original_exports = std::mem::take(&mut exec_state.module_exports);
let result = self
.inner_execute(&program, exec_state, crate::executor::BodyType::Root)
.await;
let module_exports = std::mem::replace(&mut exec_state.module_exports, original_exports);
let module_memory = std::mem::replace(&mut exec_state.memory, original_memory);
self.engine.replace_execution_kind(original_execution);
exec_state.import_stack.pop();
result.map_err(|err| {
if let KclError::ImportCycle(_) = err {
// It was an import cycle. Keep the original message.
err.override_source_ranges(vec![source_range])
} else {
KclError::Semantic(KclErrorDetails {
message: format!(
"Error loading imported file. Open it to view more details. {path}: {}",
err.message()
),
source_ranges: vec![source_range],
})
}
})?;
(module_memory, module_exports)
};
for import_item in &import_stmt.items {
// Extract the item from the module.
let item = module_memory
.get(&import_item.name.name, import_item.into())
.map_err(|_err| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("{} is not defined in module", import_item.name.name),
source_ranges: vec![SourceRange::from(&import_item.name)],
})
})?;
// Check that the item is allowed to be imported.
if !module_exports.contains(&import_item.name.name) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
source_ranges: vec![SourceRange::from(&import_item.name)],
}));
}
// Add the item to the current module.
exec_state.memory.add(
import_item.identifier(),
item.clone(),
SourceRange::from(&import_item.name),
)?;
}
last_expr = None;
}
BodyItem::ExpressionStatement(expression_statement) => {
let metadata = Metadata::from(expression_statement);
last_expr = Some(
@ -2174,21 +2044,7 @@ impl ExecutorContext {
StatementKind::Declaration { name: &var_name },
)
.await?;
let is_function = memory_item.is_function();
exec_state.memory.add(&var_name, memory_item, source_range)?;
// Track exports.
match variable_declaration.visibility {
ItemVisibility::Export => {
if !is_function {
return Err(KclError::Semantic(KclErrorDetails {
message: "Only functions can be exported".to_owned(),
source_ranges: vec![source_range],
}));
}
exec_state.module_exports.insert(var_name);
}
ItemVisibility::Default => {}
}
}
last_expr = None;
}
@ -2274,7 +2130,6 @@ impl ExecutorContext {
},
},
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
@ -2293,9 +2148,8 @@ impl ExecutorContext {
&self,
program: &Program,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<TakeSnapshot> {
let _ = self.run(program, None, id_generator, project_directory).await?;
let _ = self.run(program, None, id_generator).await?;
// Zoom to fit.
self.engine
@ -2440,7 +2294,7 @@ mod tests {
settings: Default::default(),
context_type: ContextType::Mock,
};
let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?;
let exec_state = ctx.run(&program, None, IdGenerator::default()).await?;
Ok(exec_state.memory)
}

View File

@ -37,19 +37,6 @@ impl FileSystem for FileManager {
})
}
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<String, KclError> {
tokio::fs::read_to_string(&path).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to read file `{}`: {}", path.as_ref().display(), e),
source_ranges: vec![source_range],
})
})
}
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,

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