Compare commits

...

15 Commits

Author SHA1 Message Date
078ffa02b0 Add the at token 2024-11-26 16:19:00 -06:00
d461b09a4d KCL refactor: break typechecking into its own fn (#4587) 2024-11-26 16:02:31 -06:00
9c42c39ba9 Fix publish path in release bucket (#4585) 2024-11-26 15:11:26 -05:00
aa3f40e22c KCL: Two tiny refactors (#4580)
* Refactor: Combine two impl blocks

* Refactor: Constant for NO_META(data)
2024-11-26 12:27:09 -06:00
4423ae16dc Add offset plane point-and-click user flow (#4552)
* Add a code mod for offset plane

* Add support for default plane selections to our `otherSelections` object

* Make availableVars work without a selection range
(because default planes don't have one)

* Make default planes selectable in cmdbar even if AST is empty

* Add offset plane command and activate in toolbar

* Avoid unnecessary error when sketching on offset plane by returning early

* Add supporting test features for offset plane E2E test

* Add WIP E2E test for offset plane
Struggling to get local electron test suite running properly

* Typos

* Lints

* Fix test by making it a web-based one:
I couldn't use the cmdBar fixture with an electron test for some reason.

* Update src/lib/commandBarConfigs/modelingCommandConfig.ts

* Update src/machines/modelingMachine.ts

* Revert changes to `homePageFixture`, as they were unused

* @Irev-Dev feedback: convert action to actor, fix machine layout

* Update plane icon to be not dashed, follow conventions closer
2024-11-26 16:36:14 +00:00
1d45bed649 Bump react-router-dom from 6.27.0 to 6.28.0 (#4414)
* Bump react-router-dom from 6.27.0 to 6.28.0

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.27.0 to 6.28.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@6.28.0/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.28.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Force 6.28.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2024-11-26 11:06:59 -05:00
64aac7bccc KCL refactor: Type alias for KCL object fields (#4579)
This way, if we want to change our key-value
representation later (e.g. using a tree map instead
of a hash map) we can, easily, in just one place.
2024-11-26 09:51:43 -06:00
002edeaf19 fix selection bugs found by QA-wolf (#4578)
* fix selection bugs found by QA-wolf

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* trigger ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-26 04:43:28 -05:00
5424252dac AST: Factor shebangs out of non-code metadata and into Program (#4557)
* AST: Factor shebangs out of non-code metadata and into Progam

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Empty commit to try to unstick CI

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-26 03:39:57 +00:00
30bc85add8 Add warnings for recently deprecated syntax (#4560)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-26 14:59:40 +13:00
39a2bd685b Bump tokio from 1.40.0 to 1.41.1 in /src/wasm-lib (#4426)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.40.0 to 1.41.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.40.0...tokio-1.41.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 18:45:39 -06:00
23a3e330f6 KCL tests: take fancier snapshots of KCL errors (#4574)
Right now our KCL tests output a debug representation of the KCLError
value. This works OK, but it's difficult to read an error like
"runtime error: SourceRange([44, 48])" because I don't fucking know what
the 44th character in my KCL program is.

In the modeling app, source ranges are turned into nice red squiggly
underlines in the editor. I want nice squiggly underline when I run the
Rust unit tests too, damnit. The JS world should NEVER have fancy toys
that I, a Rust programmer, cannot access. I deserve this. I need this.

So anyway instead of snapshotting debug repr, snapshot a fancy error
via the miette library.
2024-11-25 17:28:57 -06:00
99dd8b87dc Change to use Angle type's comparison (#4575) 2024-11-25 23:17:47 +00:00
5ff1d9e268 Open about section links externally in Settings (#4571)
* Open nightly download link externally

* Applied to other about links
2024-11-25 16:14:40 -05:00
ce1a37e0bc Upgrade to typescript 5.7.2 (#4569)
* Upgrade to typescript 5.7.2

* Fix tsc errors
2024-11-25 20:37:04 +00:00
68 changed files with 1447 additions and 568 deletions

View File

@ -123,7 +123,7 @@ jobs:
path: out
glob: '*'
parent: false
destination: 'dl.kittycad.io/releases/modeling-app/test/new-workflow'
destination: 'dl.kittycad.io/releases/modeling-app'
- name: Invalidate bucket cache on latest*.yml and last_download.json files
run: |

View File

@ -96310,6 +96310,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -97944,25 +97952,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -98173,6 +98162,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -100282,6 +100293,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -101916,25 +101935,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -102145,6 +102145,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -104258,6 +104280,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -105892,25 +105922,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -106121,6 +106132,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -137489,6 +137522,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -139123,25 +139164,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -139352,6 +139374,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -142077,6 +142121,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -143711,25 +143763,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -143940,6 +143973,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -146666,6 +146721,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -148300,25 +148363,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -148529,6 +148573,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -150653,6 +150719,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -152287,25 +152361,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -152516,6 +152571,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -155241,6 +155318,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -156875,25 +156960,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -157104,6 +157170,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -159213,6 +159301,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -160847,25 +160943,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -161076,6 +161153,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [
@ -163802,6 +163901,14 @@
"nonCodeMeta": {
"$ref": "#/components/schemas/NonCodeMeta"
},
"shebang": {
"allOf": [
{
"$ref": "#/components/schemas/Shebang"
}
],
"nullable": true
},
"digest": {
"type": "array",
"items": {
@ -165436,25 +165543,6 @@
},
"NonCodeValue": {
"oneOf": [
{
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"shebang"
]
},
"value": {
"type": "string"
}
}
},
{
"description": "An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.",
"type": "object",
@ -165665,6 +165753,28 @@
}
]
},
"Shebang": {
"description": "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```",
"type": "object",
"required": [
"content"
],
"properties": {
"content": {
"type": "string"
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"ProgramMemory": {
"type": "object",
"required": [

View File

@ -11,23 +11,6 @@ layout: manual
**This schema accepts exactly one of the following:**
A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `shebang`| | No |
| `value` |`string`| | No |
----
An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.
**Type:** `object`

View File

@ -18,6 +18,7 @@ A KCL program top level, or function body.
|----------|------|-------------|----------|
| `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No |
| `shebang` |[`Shebang`](/docs/kcl/types/Shebang)| | 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 |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

23
docs/kcl/types/Shebang.md Normal file
View File

@ -0,0 +1,23 @@
---
title: "Shebang"
excerpt: "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```"
layout: manual
---
A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `content` |`string`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -35,7 +35,7 @@ export class CmdBarFixture {
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
const reviewForm = await this.page.locator('#review-form')
const reviewForm = this.page.locator('#review-form')
const getHeaderArgs = async () => {
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
const entries = await Promise.all(

View File

@ -6,6 +6,7 @@ export class ToolbarFixture {
public page: Page
extrudeButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
rectangleBtn!: Locator
@ -25,6 +26,7 @@ export class ToolbarFixture {
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line')
this.rectangleBtn = page.getByTestId('corner-rectangle')

View File

@ -551,3 +551,53 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
)
})
})
test(`Offset plane point-and-click`, async ({
app,
scene,
editor,
toolbar,
cmdBar,
}) => {
await app.initialise()
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
await test.step(`Look for the blue of the XZ plane`, async () => {
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.offsetPlaneButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'plane',
currentArgValue: '',
headerArguments: { Plane: '', Distance: '' },
highlightedHeaderArg: 'plane',
commandName: 'Offset plane',
})
await clickOnXzPlane()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'distance',
currentArgValue: '5',
headerArguments: { Plane: '1 plane', Distance: '' },
highlightedHeaderArg: 'distance',
commandName: 'Offset plane',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(expectedOutput)
await editor.expectState({
diagnostics: [],
activeLines: [expectedOutput],
highlightedCode: '',
})
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -56,7 +56,7 @@
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.27.0",
"react-router-dom": "^6.28.0",
"sketch-helpers": "^0.0.4",
"three": "^0.166.1",
"ua-parser-js": "^1.0.37",
@ -205,7 +205,7 @@
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.0.0",
"typescript": "^5.7.2",
"vite": "^5.4.6",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",

View File

@ -24,7 +24,7 @@
"@lezer/highlight": "^1.2.0",
"@ts-stack/markdown": "^1.5.0",
"json-rpc-2.0": "^1.7.0",
"typescript": "^5.5.2",
"typescript": "^5.7.2",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8"
},

View File

@ -26,7 +26,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
private async start(): Promise<void> {
let contentLength: null | number = null
let buffer = new Uint8Array()
let buffer: Uint8Array = new Uint8Array()
for await (const bytes of this) {
buffer = Bytes.append(Uint8Array, buffer, bytes)

View File

@ -95,6 +95,10 @@ export default class Queue<T>
return this
}
[Symbol.asyncDispose](): Promise<void> {
return this.close()
}
get locked(): boolean {
return this.#stream.locked
}

View File

@ -182,10 +182,10 @@ tslib@^2.3.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
typescript@^5.5.2:
version "5.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507"
integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
undici-types@~5.26.4:
version "5.26.5"

View File

@ -22,7 +22,7 @@ import {
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext'
import * as TWEEN from '@tweenjs/tween.js'
import { Axis } from 'lib/selections'
import { Axis, NonCodeSelection } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes'
import { CameraControls } from './CameraControls'
import { EngineCommandManager } from 'lang/std/engineConnection'
@ -654,7 +654,7 @@ export class SceneInfra {
await this.onClickCallback({ mouseEvent, intersects })
}
}
updateOtherSelectionColors = (otherSelections: Axis[]) => {
updateOtherSelectionColors = (otherSelections: NonCodeSelection[]) => {
const axisGroup = this.scene.children.find(
({ userData }) => userData?.type === AXIS_GROUP
)

View File

@ -1,21 +1,26 @@
import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclProvider'
import { Artifact } from 'lang/std/artifactGraph'
import { CommandArgument } from 'lib/commandTypes'
import {
canSubmitSelectionArg,
getSelectionType,
getSelectionCountByType,
getSelectionTypeDisplayText,
} from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useMemo, useRef, useState } from 'react'
import { StateFrom } from 'xstate'
const semanticEntityNames: { [key: string]: Array<Artifact['type']> } = {
const semanticEntityNames: {
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
} = {
face: ['wall', 'cap', 'solid2D'],
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
point: [],
plane: ['defaultPlane'],
}
function getSemanticSelectionType(selectionType: Array<Artifact['type']>) {
@ -43,21 +48,13 @@ function CommandBarSelectionInput({
stepBack: () => void
onSubmit: (data: unknown) => void
}) {
const { code } = useKclContext()
const inputRef = useRef<HTMLInputElement>(null)
const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => {
const selectionRangeEnd = !selection
? null
: selection?.graphSelections[0]?.codeRef?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length || !selection
? 'none'
: !selection
? 'none'
: getSelectionType(selection)
}, [selection, code])
return getSelectionCountByType(selection)
}, [selection])
const canSubmitSelection = useMemo<boolean>(
() => canSubmitSelectionArg(selectionsByType, arg),
[selectionsByType]
@ -67,6 +64,30 @@ function CommandBarSelectionInput({
inputRef.current?.focus()
}, [selection, inputRef])
// Show the default planes if the selection type is 'plane'
useEffect(() => {
if (arg.selectionTypes.includes('plane') && !canSubmitSelection) {
toSync(() => {
return Promise.all([
kclManager.showPlanes(),
kclManager.setSelectionFilter(['plane', 'object']),
])
}, reportRejection)()
}
return () => {
toSync(() => {
const promises = [
new Promise(() => kclManager.defaultSelectionFilter()),
]
if (!kclManager._isAstEmpty(kclManager.ast)) {
promises.push(kclManager.hidePlanes())
}
return Promise.all(promises)
}, reportRejection)()
}
}, [])
// Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already
useEffect(() => {
@ -109,11 +130,15 @@ function CommandBarSelectionInput({
{arg.warningMessage}
</p>
)}
<span data-testid="cmd-bar-arg-name" className="sr-only">
{arg.name}
</span>
<input
id="selection"
name="selection"
ref={inputRef}
required
data-testid="cmd-bar-arg-value"
placeholder="Select an entity with your mouse"
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
onKeyDown={(event) => {

View File

@ -818,15 +818,16 @@ const CustomIconMap = {
),
plane: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="plane"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.92871 5.11391L4.43964 5.00995V4.10898V3.60898V3.10898L4.92871 3.21293L5.41778 3.31689L6.29907 3.50421V4.00421V4.50421L5.41778 4.31689V5.21786L4.92871 5.11391ZM11.8774 4.68991L8.1585 3.89945V4.39945V4.89945L11.8774 5.68991V5.18991V4.68991ZM13.7368 5.08515V5.58515V6.08515L14.6181 6.27247V7.17344L15.1071 7.2774L15.5962 7.38135V6.48038V5.98038V5.48038L15.1071 5.37643L14.6181 5.27247L13.7368 5.08515ZM15.5962 9.28233L15.1071 9.17837L14.6181 9.07441V12.8764L15.1071 12.9803L15.5962 13.0843V9.28233ZM15.5962 14.9852L15.1071 14.8813L14.6181 14.7773V15.6783L13.7368 15.491V15.991V16.491L14.6181 16.6783L15.1071 16.7823L15.5962 16.8862V16.3862V15.8862V14.9852ZM11.8774 16.0957V15.5957V15.0957L8.1585 14.3053V14.8053V15.3053L11.8774 16.0957ZM6.29907 14.91V14.41V13.91L5.41778 13.7227V12.8217L4.92871 12.7178L4.43964 12.6138V13.5148V14.0148V14.5148L4.92871 14.6188L5.41778 14.7227L6.29907 14.91ZM4.43964 10.7129L4.92871 10.8168L5.41778 10.9208V7.11883L4.92871 7.01488L4.43964 6.91092V10.7129Z"
d="M10.9781 5.49876L14.6181 6.27247V9.99381L10.9781 9.22011V5.49876ZM10 4.29085L10.9781 4.49876L14.6181 5.27247L14.6182 5.27247L15.5963 5.48038H15.5963V6.48038V10.2017V11.2017L15.5963 11.2017V15.8862V16.8862L14.6181 16.6783L5.41784 14.7227L4.4397 14.5148V13.5148V4.10898V3.10898L5.41784 3.31689L10 4.29085ZM14.6181 10.9938V15.6783L5.41784 13.7227V4.31689L10 5.29085V9.0122V10.0122L10.9781 10.2201L14.6181 10.9938Z"
fill="currentColor"
/>
</svg>

View File

@ -317,6 +317,7 @@ export const ModelingMachineProvider = ({
})
})
}
let selections: Selections = {
graphSelections: [],
otherSelections: [],
@ -375,7 +376,10 @@ export const ModelingMachineProvider = ({
}
}
if (setSelections.selectionType === 'otherSelection') {
if (
setSelections.selectionType === 'axisSelection' ||
setSelections.selectionType === 'defaultPlaneSelection'
) {
if (editorManager.isShiftDown) {
selections = {
graphSelections: selectionRanges.graphSelections,
@ -387,20 +391,11 @@ export const ModelingMachineProvider = ({
otherSelections: [setSelections.selection],
}
}
const { engineEvents, updateSceneObjectColors } =
handleSelectionBatch({
selections: selections,
})
engineEvents &&
engineEvents.forEach((event) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand(event)
})
updateSceneObjectColors()
return {
selectionRanges: selections,
}
}
if (setSelections.selectionType === 'completeSelection') {
editorManager.selectRange(setSelections.selection)
if (!sketchDetails)

View File

@ -24,6 +24,7 @@ import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel
@ -245,6 +246,9 @@ export const AllSettingsFields = forwardRef(
to inject the version from package.json */}
App version {APP_VERSION}.{' '}
<a
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank"
rel="noopener noreferrer"
@ -255,6 +259,9 @@ export const AllSettingsFields = forwardRef(
<p className="max-w-2xl mt-6">
Don't see the feature you want? Check to see if it's on{' '}
<a
onClick={openExternalBrowserIfDesktop(
'https://github.com/KittyCAD/modeling-app/discussions'
)}
href="https://github.com/KittyCAD/modeling-app/discussions"
target="_blank"
rel="noopener noreferrer"
@ -269,6 +276,9 @@ export const AllSettingsFields = forwardRef(
Want to experience the latest and (hopefully) greatest from our
main development branch?{' '}
<a
onClick={openExternalBrowserIfDesktop(
'https://zoo.dev/modeling-app/download/nightly'
)}
href="https://zoo.dev/modeling-app/download/nightly"
target="_blank"
rel="noopener noreferrer"

View File

@ -17,6 +17,7 @@ import {
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
import { useCommandsContext } from 'hooks/useCommandsContext'
enum StreamState {
Playing = 'playing',
@ -30,6 +31,7 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext()
const { state, send } = useModelingContext()
const { commandBarState } = useCommandsContext()
const { mediaStream } = useAppStream()
const { overallState, immediateState } = useNetworkContext()
const [streamState, setStreamState] = useState(StreamState.Unset)
@ -260,7 +262,15 @@ export const Stream = () => {
if (!videoRef.current) return
// If we're in sketch mode, don't send a engine-side select event
if (state.matches('Sketch')) return
if (state.matches({ idle: 'showPlanes' })) return
// Only respect default plane selection if we're on a selection command argument
if (
state.matches({ idle: 'showPlanes' }) &&
!(
commandBarState.matches('Gathering arguments') &&
commandBarState.context.currentArgument?.inputType === 'selection'
)
)
return
// If we're mousing up from a camera drag, don't send a select event
if (sceneInfra.camControls.wasDragging === true) return

View File

@ -169,6 +169,7 @@ export function useEngineConnectionSubscriptions() {
pathToNode: artifact.codeRef.pathToNode,
},
})
return
}
// Artifact is likely an extrusion face

View File

@ -23,6 +23,7 @@ import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
import { markOnce } from 'lib/performance'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { EntityType_type } from '@kittycad/lib/dist/types/src/models'
interface ExecuteArgs {
ast?: Node<Program>
@ -37,6 +38,7 @@ interface ExecuteArgs {
export class KclManager {
private _ast: Node<Program> = {
body: [],
shebang: null,
start: 0,
end: 0,
moduleId: 0,
@ -204,6 +206,7 @@ export class KclManager {
clearAst() {
this._ast = {
body: [],
shebang: null,
start: 0,
end: 0,
moduleId: 0,
@ -279,7 +282,7 @@ export class KclManager {
this.lints = await lintAst({ ast: ast })
sceneInfra.modelingSend({ type: 'code edit during sketch' })
defaultSelectionFilter(execState.memory, this.engineCommandManager)
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
if (args.zoomToFit) {
let zoomObjectId: string | undefined = ''
@ -566,8 +569,13 @@ export class KclManager {
}
return Promise.all(thePromises)
}
/** TODO: this function is hiding unawaited asynchronous work */
defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
setSelectionFilterToDefault(this.programMemory, this.engineCommandManager)
}
/** TODO: this function is hiding unawaited asynchronous work */
setSelectionFilter(filter: EntityType_type[]) {
setSelectionFilter(filter, this.engineCommandManager)
}
/**
@ -589,18 +597,35 @@ export class KclManager {
}
}
function defaultSelectionFilter(
const defaultSelectionFilter: EntityType_type[] = [
'face',
'edge',
'solid2d',
'curve',
'object',
]
/** TODO: This function is not synchronous but is currently treated as such */
function setSelectionFilterToDefault(
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
programMemory.hasSketchOrSolid() &&
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter: ['face', 'edge', 'solid2d', 'curve'],
},
})
setSelectionFilter(defaultSelectionFilter, engineCommandManager)
}
/** TODO: This function is not synchronous but is currently treated as such */
function setSelectionFilter(
filter: EntityType_type[],
engineCommandManager: EngineCommandManager
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter,
},
})
}

View File

@ -123,6 +123,7 @@ describe('Testing addSketchTo', () => {
const result = addSketchTo(
{
body: [],
shebang: null,
start: 0,
end: 0,
moduleId: 0,

View File

@ -527,6 +527,45 @@ export function sketchOnExtrudedFace(
}
}
/**
* Append an offset plane to the AST
*/
export function addOffsetPlane({
node,
defaultPlane,
offset,
}: {
node: Node<Program>
defaultPlane: DefaultPlaneStr
offset: Expr
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node)
const newPlaneName = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.PLANE)
const newPlane = createVariableDeclaration(
newPlaneName,
createCallExpressionStdLib('offsetPlane', [
createLiteral(defaultPlane.toUpperCase()),
offset,
])
)
modifiedAst.body.push(newPlane)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}
/**
* Modify the AST to create a new sketch using the variable declaration
* of an offset plane. The new sketch just has to come after the offset

View File

@ -1823,6 +1823,7 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
modifiedAst: {
start: 0,
end: 0,
shebang: null,
moduleId: 0,
body: [],

View File

@ -40,6 +40,10 @@ export type ModelingCommandSchema = {
selection: Selections
radius: KclCommandValue
}
'Offset plane': {
plane: Selections
distance: KclCommandValue
}
'change tool': {
tool: SketchTool
}
@ -276,6 +280,24 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
},
},
},
'Offset plane': {
description: 'Offset a plane.',
icon: 'plane',
args: {
plane: {
inputType: 'selection',
selectionTypes: ['plane'],
multiple: false,
required: true,
skip: true,
},
distance: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
Fillet: {
description: 'Fillet edge',
icon: 'fillet',

View File

@ -54,6 +54,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
EXTRUDE: 'extrude',
SEGMENT: 'seg',
REVOLVE: 'revolve',
PLANE: 'plane',
} as const
/** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5`

View File

@ -21,6 +21,7 @@ import {
} from 'lang/queryAst'
import { CommandArgument } from './commandTypes'
import {
DefaultPlaneStr,
getParentGroup,
SEGMENT_BODIES_PLUS_PROFILE_START,
} from 'clientSideScene/sceneEntities'
@ -46,6 +47,10 @@ export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type DefaultPlaneSelection = {
name: DefaultPlaneStr
id: string
}
/** @deprecated Use {@link Artifact} instead. */
type Selection__old =
@ -72,9 +77,11 @@ type Selection__old =
// TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836
secondaryRange: SourceRange
}
export type NonCodeSelection = Axis | DefaultPlaneSelection
/** @deprecated Use {@link Selection} instead. */
export type Selections__old = {
otherSelections: Axis[]
otherSelections: NonCodeSelection[]
codeBasedSelections: Selection__old[]
}
export interface Selection {
@ -82,7 +89,7 @@ export interface Selection {
codeRef: CodeRef
}
export type Selections = {
otherSelections: Array<Axis>
otherSelections: Array<NonCodeSelection>
graphSelections: Array<Selection>
}
@ -172,11 +179,31 @@ export async function getEventForSelectWithPoint({
return {
type: 'Set selection',
data: {
selectionType: 'otherSelection',
selectionType: 'axisSelection',
selection: X_AXIS_UUID === data.entity_id ? 'x-axis' : 'y-axis',
},
}
}
// Check for default plane selection
const foundDefaultPlane =
engineCommandManager.defaultPlanes !== null &&
Object.entries(engineCommandManager.defaultPlanes).find(
([, plane]) => plane === data.entity_id
)
if (foundDefaultPlane) {
return {
type: 'Set selection',
data: {
selectionType: 'defaultPlaneSelection',
selection: {
name: foundDefaultPlane[0] as DefaultPlaneStr,
id: data.entity_id,
},
},
}
}
let _artifact = engineCommandManager.artifactGraph.get(data.entity_id)
const codeRefs = getCodeRefsByArtifactId(
data.entity_id,
@ -207,7 +234,7 @@ export function getEventForSegmentSelection(
return {
type: 'Set selection',
data: {
selectionType: 'otherSelection',
selectionType: 'axisSelection',
selection: obj?.userData?.type === X_AXIS ? 'x-axis' : 'y-axis',
},
}
@ -272,7 +299,6 @@ export function handleSelectionBatch({
}): {
engineEvents: Models['WebSocketRequest_type'][]
codeMirrorSelection: EditorSelection
otherSelections: Axis[]
updateSceneObjectColors: () => void
} {
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
@ -303,7 +329,6 @@ export function handleSelectionBatch({
ranges,
selections.graphSelections.length - 1
),
otherSelections: selections.otherSelections,
updateSceneObjectColors: () =>
updateSceneObjectColors(selections.graphSelections),
}
@ -314,7 +339,6 @@ export function handleSelectionBatch({
0
),
engineEvents,
otherSelections: selections.otherSelections,
updateSceneObjectColors: () =>
updateSceneObjectColors(selections.graphSelections),
}
@ -536,7 +560,8 @@ export function canSweepSelection(selection: Selections) {
}
// This accounts for non-geometry selections under "other"
export type ResolvedSelectionType = [Artifact['type'] | 'other', number]
export type ResolvedSelectionType = Artifact['type'] | 'other'
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
/**
* In the future, I'd like this function to properly return the type of each selected entity based on
@ -545,28 +570,48 @@ export type ResolvedSelectionType = [Artifact['type'] | 'other', number]
* @param selection
* @returns
*/
export function getSelectionType(
export function getSelectionCountByType(
selection?: Selections
): ResolvedSelectionType[] {
if (!selection) return []
const selectionsWithArtifacts = selection.graphSelections.filter(
(s) => !!s.artifact
): SelectionCountsByType | 'none' {
const selectionsByType: SelectionCountsByType = new Map()
if (
!selection ||
(!selection.graphSelections.length && !selection.otherSelections.length)
)
const firstSelection = selectionsWithArtifacts[0]
const firstSelectionType = firstSelection?.artifact?.type
if (!firstSelectionType) return []
const selectionsWithSameType = selectionsWithArtifacts.filter(
(s) => s.artifact?.type === firstSelection.artifact?.type
)
return [[firstSelectionType, selectionsWithSameType.length]]
return 'none'
function incrementOrInitializeSelectionType(type: ResolvedSelectionType) {
const count = selectionsByType.get(type) || 0
selectionsByType.set(type, count + 1)
}
selection.otherSelections.forEach((selection) => {
if (typeof selection === 'string') {
incrementOrInitializeSelectionType('other')
} else if ('name' in selection) {
incrementOrInitializeSelectionType('plane')
}
})
selection.graphSelections.forEach((selection) => {
if (!selection.artifact) {
incrementOrInitializeSelectionType('other')
return
}
incrementOrInitializeSelectionType(selection.artifact.type)
})
return selectionsByType
}
export function getSelectionTypeDisplayText(
selection?: Selections
): string | null {
const selectionsByType = getSelectionType(selection)
const selectionsByType = getSelectionCountByType(selection)
if (selectionsByType === 'none') return null
return (selectionsByType as Exclude<typeof selectionsByType, 'none'>)
return selectionsByType
.entries()
.map(
// Hack for showing "face" instead of "extrude-wall" in command bar text
([type, count]) =>
@ -575,16 +620,17 @@ export function getSelectionTypeDisplayText(
.replace('solid2D', 'face')
.replace('segment', 'face')}${count > 1 ? 's' : ''}`
)
.toArray()
.join(', ')
}
export function canSubmitSelectionArg(
selectionsByType: 'none' | ResolvedSelectionType[],
selectionsByType: 'none' | Map<ResolvedSelectionType, number>,
argument: CommandArgument<unknown> & { inputType: 'selection' }
) {
return (
selectionsByType !== 'none' &&
selectionsByType.every(([type, count]) => {
selectionsByType.entries().every(([type, count]) => {
const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
return (
foundIndex !== -1 &&

View File

@ -252,10 +252,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[
{
id: 'plane-offset',
onClick: () =>
console.error('Plane through normal not yet implemented'),
onClick: ({ commandBarSend }) => {
commandBarSend({
type: 'Find and select command',
data: { name: 'Offset plane', groupId: 'modeling' },
})
},
hotkey: 'O',
icon: 'plane',
status: 'unavailable',
status: 'available',
title: 'Offset plane',
description: 'Create a plane parallel to an existing plane.',
links: [],

View File

@ -34,6 +34,8 @@ export function useCalculateKclExpression({
} {
const { programMemory, code } = useKclContext()
const { context } = useModelingContext()
// If there is no selection, use the end of the code
// so all variables are available
const selectionRange:
| (typeof context)['selectionRanges']['graphSelections'][number]['codeRef']['range']
| undefined = context.selectionRanges.graphSelections[0]?.codeRef?.range
@ -72,11 +74,12 @@ export function useCalculateKclExpression({
}, [programMemory, newVariableName])
useEffect(() => {
if (!programMemory || !selectionRange) return
if (!programMemory) return
const varInfo = findAllPreviousVariables(
kclManager.ast,
kclManager.programMemory,
selectionRange
// If there is no selection, use the end of the code
selectionRange || [code.length, code.length]
)
setAvailableVarInfo(varInfo)
}, [kclManager.ast, kclManager.programMemory, selectionRange])

File diff suppressed because one or more lines are too long

111
src/wasm-lib/Cargo.lock generated
View File

@ -228,6 +228,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "backtrace-ext"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
dependencies = [
"backtrace",
]
[[package]]
name = "base64"
version = "0.13.1"
@ -1625,6 +1634,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1708,6 +1723,7 @@ dependencies = [
"kittycad-modeling-cmds",
"lazy_static",
"measurements",
"miette",
"mime_guess",
"parse-display 0.9.1",
"pretty_assertions",
@ -1971,6 +1987,37 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miette"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
dependencies = [
"backtrace",
"backtrace-ext",
"cfg-if",
"miette-derive",
"owo-colors",
"supports-color",
"supports-hyperlinks",
"supports-unicode",
"terminal_size",
"textwrap",
"thiserror 1.0.68",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -2163,6 +2210,12 @@ dependencies = [
"thiserror 1.0.68",
]
[[package]]
name = "owo-colors"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
[[package]]
name = "papergrid"
version = "0.11.0"
@ -3311,6 +3364,12 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "socket2"
version = "0.5.7"
@ -3396,6 +3455,27 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "supports-color"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
dependencies = [
"is_ci",
]
[[package]]
name = "supports-hyperlinks"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee"
[[package]]
name = "supports-unicode"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "1.0.109"
@ -3496,6 +3576,27 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width 0.1.14",
]
[[package]]
name = "thiserror"
version = "1.0.68"
@ -3614,9 +3715,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.40.0"
version = "1.41.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
dependencies = [
"backtrace",
"bytes",
@ -3955,6 +4056,12 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"

View File

@ -16,7 +16,7 @@ gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["sync"] }
tokio = { version = "1.41.1", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
@ -29,7 +29,7 @@ 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"] }
tokio = { version = "1.41.1", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }

View File

@ -12,4 +12,4 @@ kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }

View File

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

View File

@ -33,6 +33,7 @@ kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
lazy_static = "1.5.0"
measurements = "0.11.0"
miette = "7.2.0"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true }
@ -55,7 +56,7 @@ zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.72" }
tokio = { version = "1.40.0", features = ["sync", "time"] }
tokio = { version = "1.41.1", 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"
@ -64,7 +65,7 @@ web-sys = { version = "0.3.72", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
tokio = { version = "1.39.3", features = ["full"] }
tokio = { version = "1.41.1", features = ["full"] }
tokio-tungstenite = { version = "0.24.0", features = ["rustls-tls-native-roots"] }
tower-lsp = { version = "0.20.0", features = ["proposed"] }
@ -89,8 +90,9 @@ iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0"
miette = { version = "7.2.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
tokio = { version = "1.41.1", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8.0"
[[bench]]

View File

@ -65,6 +65,10 @@ impl<T> Node<T> {
source_range: SourceRange([self.start, self.end, self.module_id.0 as usize]),
}
}
pub fn contains(&self, pos: usize) -> bool {
self.start <= pos && pos <= self.end
}
}
impl<T: JsonSchema> schemars::JsonSchema for Node<T> {
@ -117,8 +121,12 @@ impl<T> Node<T> {
})
}
pub fn as_source_range(&self) -> SourceRange {
SourceRange([self.start, self.end, self.module_id.as_usize()])
}
pub fn as_source_ranges(&self) -> Vec<SourceRange> {
vec![SourceRange([self.start, self.end, self.module_id.as_usize()])]
vec![self.as_source_range()]
}
}
@ -173,6 +181,8 @@ pub struct Program {
pub body: Vec<BodyItem>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shebang: Option<Node<Shebang>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@ -261,19 +271,14 @@ impl Program {
}
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
// Check if we are in the non code meta.
if let Some(meta) = self.get_non_code_meta_for_position(pos) {
for node in &meta.start_nodes {
if node.contains(pos) {
// We only care about the shebang.
if let NonCodeValue::Shebang { value: _ } = &node.value {
let source_range: SourceRange = node.into();
return Some(Hover::Comment {
value: r#"The `#!` at the start of a script, known as a shebang, specifies the path to the interpreter that should execute the script. This line is not necessary for your `kcl` to run in the modeling-app. You can safely delete it. If you wish to learn more about what you _can_ do with a shebang, read this doc: [zoo.dev/docs/faq/shebang](https://zoo.dev/docs/faq/shebang)."#.to_string(),
range: source_range.to_lsp_range(code),
});
}
}
// Check if we are in shebang.
if let Some(node) = &self.shebang {
if node.contains(pos) {
let source_range: SourceRange = node.into();
return Some(Hover::Comment {
value: r#"The `#!` at the start of a script, known as a shebang, specifies the path to the interpreter that should execute the script. This line is not necessary for your `kcl` to run in the modeling-app. You can safely delete it. If you wish to learn more about what you _can_ do with a shebang, read this doc: [zoo.dev/docs/faq/shebang](https://zoo.dev/docs/faq/shebang)."#.to_string(),
range: source_range.to_lsp_range(code),
});
}
}
@ -528,6 +533,26 @@ impl Program {
}
}
/// A shebang.
/// This is a special type of comment that is at the top of the file.
/// It looks like this:
/// ```python,no_run
/// #!/usr/bin/env python
/// ```
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, ts_rs::TS, JsonSchema, Bake)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
pub struct Shebang {
pub content: String,
}
impl Shebang {
pub fn new(content: String) -> Self {
Shebang { content }
}
}
/// Identifier of a source file. Uses a u32 to keep the size small.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, ts_rs::TS, JsonSchema, Bake)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
@ -948,13 +973,8 @@ pub struct NonCodeNode {
}
impl Node<NonCodeNode> {
pub fn contains(&self, pos: usize) -> bool {
self.start <= pos && pos <= self.end
}
pub fn format(&self, indentation: &str) -> String {
match &self.value {
NonCodeValue::Shebang { value } => format!("{}\n\n", value),
NonCodeValue::InlineComment {
value,
style: CommentStyle::Line,
@ -994,7 +1014,6 @@ impl Node<NonCodeNode> {
impl NonCodeNode {
pub fn value(&self) -> String {
match &self.value {
NonCodeValue::Shebang { value } => value.clone(),
NonCodeValue::InlineComment { value, style: _ } => value.clone(),
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
@ -1028,15 +1047,6 @@ impl CommentStyle {
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum NonCodeValue {
/// A shebang.
/// This is a special type of comment that is at the top of the file.
/// It looks like this:
/// ```python,no_run
/// #!/usr/bin/env python
/// ```
Shebang {
value: String,
},
/// An inline comment.
/// Here are examples:
/// `1 + 1 // This is an inline comment`.
@ -3338,6 +3348,7 @@ const cylinder = startSketchOn('-XZ')
body: Node::no_src(Program {
body: Vec::new(),
non_code_meta: Default::default(),
shebang: None,
digest: None,
}),
return_type: None,
@ -3361,6 +3372,7 @@ const cylinder = startSketchOn('-XZ')
inner: Program {
body: Vec::new(),
non_code_meta: Default::default(),
shebang: None,
digest: None,
},
start: 0,
@ -3388,6 +3400,7 @@ const cylinder = startSketchOn('-XZ')
inner: Program {
body: Vec::new(),
non_code_meta: Default::default(),
shebang: None,
digest: None,
},
start: 0,
@ -3426,6 +3439,7 @@ const cylinder = startSketchOn('-XZ')
inner: Program {
body: Vec::new(),
non_code_meta: Default::default(),
shebang: None,
digest: None,
},
start: 0,

View File

@ -66,6 +66,9 @@ impl Program {
for body_item in slf.body.iter_mut() {
hasher.update(body_item.compute_digest());
}
if let Some(shebang) = &slf.shebang {
hasher.update(&shebang.inner.content);
}
hasher.update(slf.non_code_meta.compute_digest());
});
}
@ -207,9 +210,6 @@ impl ReturnStatement {
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());

View File

@ -54,10 +54,56 @@ pub enum KclError {
Internal(KclErrorDetails),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[derive(thiserror::Error, Debug)]
#[error("{}", self.error.get_message())]
pub struct Report {
pub error: KclError,
pub kcl_source: String,
pub filename: String,
}
impl miette::Diagnostic for Report {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let family = match self.error {
KclError::Lexical(_) => "Lexical",
KclError::Syntax(_) => "Syntax",
KclError::Semantic(_) => "Semantic",
KclError::ImportCycle(_) => "ImportCycle",
KclError::Type(_) => "Type",
KclError::Unimplemented(_) => "Unimplemented",
KclError::Unexpected(_) => "Unexpected",
KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
KclError::UndefinedValue(_) => "UndefinedValue",
KclError::InvalidExpression(_) => "InvalidExpression",
KclError::Engine(_) => "Engine",
KclError::Internal(_) => "Internal",
};
let error_string = format!("KCL {family} error");
Some(Box::new(error_string))
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.kcl_source)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let iter = self
.error
.source_ranges()
.clone()
.into_iter()
.map(miette::SourceSpan::from)
.map(|span| miette::LabeledSpan::new_with_span(None, span));
Some(Box::new(iter))
}
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
#[error("{message}")]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
#[label(collection, "Errors")]
pub source_ranges: Vec<SourceRange>,
#[serde(rename = "msg")]
pub message: String,

View File

@ -197,24 +197,17 @@ pub struct Environment {
parent: Option<EnvironmentRef>,
}
const NO_META: Vec<Metadata> = Vec::new();
impl Environment {
pub fn root() -> Self {
Self {
// Prelude
bindings: HashMap::from([
("ZERO".to_string(), KclValue::from_number(0.0, Default::default())),
(
"QUARTER_TURN".to_string(),
KclValue::from_number(90.0, Default::default()),
),
(
"HALF_TURN".to_string(),
KclValue::from_number(180.0, Default::default()),
),
(
"THREE_QUARTER_TURN".to_string(),
KclValue::from_number(270.0, Default::default()),
),
("ZERO".to_string(), KclValue::from_number(0.0, NO_META)),
("QUARTER_TURN".to_string(), KclValue::from_number(90.0, NO_META)),
("HALF_TURN".to_string(), KclValue::from_number(180.0, NO_META)),
("THREE_QUARTER_TURN".to_string(), KclValue::from_number(270.0, NO_META)),
]),
parent: None,
}
@ -1020,6 +1013,20 @@ impl From<[usize; 3]> for SourceRange {
}
}
impl From<&SourceRange> for miette::SourceSpan {
fn from(source_range: &SourceRange) -> Self {
let length = source_range.end() - source_range.start();
let start = miette::SourceOffset::from(source_range.start());
Self::new(start, length)
}
}
impl From<SourceRange> for miette::SourceSpan {
fn from(source_range: SourceRange) -> Self {
Self::from(&source_range)
}
}
impl SourceRange {
/// Create a new source range.
pub fn new(start: usize, end: usize, module_id: ModuleId) -> Self {
@ -3177,6 +3184,7 @@ let w = f() + f()
inner: crate::ast::types::Program {
body: Vec::new(),
non_code_meta: Default::default(),
shebang: None,
digest: None,
},
start: 0,

View File

@ -13,6 +13,8 @@ use crate::{
ExecState, ExecutorContext, KclError, SourceRange,
};
pub type KclObjectFields = HashMap<String, KclValue>;
/// Any KCL value.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -49,7 +51,7 @@ pub enum KclValue {
meta: Vec<Metadata>,
},
Object {
value: HashMap<String, KclValue>,
value: KclObjectFields,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
@ -84,83 +86,6 @@ pub enum KclValue {
},
}
impl KclValue {
pub(crate) fn metadata(&self) -> Vec<Metadata> {
match self {
KclValue::Uuid { value: _, meta } => meta.clone(),
KclValue::Bool { value: _, meta } => meta.clone(),
KclValue::Number { value: _, meta } => meta.clone(),
KclValue::Int { value: _, meta } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(),
KclValue::Array { value: _, meta } => meta.clone(),
KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane(x) => x.meta.clone(),
KclValue::Face(x) => x.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid(x) => x.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::KclNone { meta, .. } => meta.clone(),
}
}
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::Array { value, .. } => {
let solids: Vec<_> = value
.iter()
.enumerate()
.map(|(i, v)| {
v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
anyhow::anyhow!(
"expected this array to only contain solids, but element {i} was actually {}",
v.human_friendly_type()
)
})
})
.collect::<Result<_, _>>()?;
Ok(SolidSet::Solids(solids))
}
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
}
}
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str {
match self {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid(_) => "Solid",
KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane",
KclValue::Face(_) => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::KclNone { .. } => "None",
}
}
pub(crate) fn is_function(&self) -> bool {
matches!(self, KclValue::Function { .. })
}
}
impl From<SketchSet> for KclValue {
fn from(sg: SketchSet) -> Self {
match sg {
@ -249,8 +174,82 @@ impl From<&KclValue> for Vec<SourceRange> {
}
impl KclValue {
pub(crate) fn metadata(&self) -> Vec<Metadata> {
match self {
KclValue::Uuid { value: _, meta } => meta.clone(),
KclValue::Bool { value: _, meta } => meta.clone(),
KclValue::Number { value: _, meta } => meta.clone(),
KclValue::Int { value: _, meta } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(),
KclValue::Array { value: _, meta } => meta.clone(),
KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane(x) => x.meta.clone(),
KclValue::Face(x) => x.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid(x) => x.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::KclNone { meta, .. } => meta.clone(),
}
}
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::Array { value, .. } => {
let solids: Vec<_> = value
.iter()
.enumerate()
.map(|(i, v)| {
v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
anyhow::anyhow!(
"expected this array to only contain solids, but element {i} was actually {}",
v.human_friendly_type()
)
})
})
.collect::<Result<_, _>>()?;
Ok(SolidSet::Solids(solids))
}
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
}
}
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str {
match self {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid(_) => "Solid",
KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane",
KclValue::Face(_) => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::KclNone { .. } => "None",
}
}
pub(crate) fn is_function(&self) -> bool {
matches!(self, KclValue::Function { .. })
}
/// Put the number into a KCL value.
pub fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta }
}
@ -287,7 +286,7 @@ impl KclValue {
}
}
pub fn as_object(&self) -> Option<&HashMap<String, KclValue>> {
pub fn as_object(&self) -> Option<&KclObjectFields> {
if let KclValue::Object { value, meta: _ } = &self {
Some(value)
} else {
@ -295,7 +294,7 @@ impl KclValue {
}
}
pub fn into_object(self) -> Option<HashMap<String, KclValue>> {
pub fn into_object(self) -> Option<KclObjectFields> {
if let KclValue::Object { value, meta: _ } = self {
Some(value)
} else {

View File

@ -15,9 +15,10 @@ use crate::{
CallExpression, CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression,
Identifier, IfExpression, ImportItem, ImportStatement, ItemVisibility, Literal, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, Node, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression,
ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator,
ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, Shebang, TagDeclarator,
UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
},
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
executor::SourceRange,
parser::{
@ -49,7 +50,6 @@ pub fn run_parser(i: TokenSlice) -> super::ParseResult {
#[derive(Debug, Clone, Default)]
pub(crate) struct ParseContext {
pub errors: Vec<ParseError>,
#[allow(dead_code)]
pub warnings: Vec<ParseError>,
}
@ -75,14 +75,34 @@ impl ParseContext {
/// Add an error to the current `ParseContext`, panics if there is none.
fn err(e: ParseError) {
// TODO follow warnings replacement with errors
CTXT.with_borrow_mut(|ctxt| ctxt.as_mut().unwrap().errors.push(e));
}
/// Add a warning to the current `ParseContext`, panics if there is none.
#[allow(dead_code)]
fn warn(mut e: ParseError) {
e.severity = error::Severity::Warning;
CTXT.with_borrow_mut(|ctxt| ctxt.as_mut().unwrap().warnings.push(e));
CTXT.with_borrow_mut(|ctxt| {
// Avoid duplicating warnings. This is possible since the parser can try one path, find
// a warning, then backtrack and decide not to take that path and try another. This can
// happen 'high up the stack', so it's impossible to fix where the warnings are generated.
// Ideally we would pass warnings up the call stack rather than use a context object or
// have some way to mark warnings as speculative or committed, but I don't think Winnow
// is flexible enough for that (or at least, not without significant changes to the
// parser).
let warnings = &mut ctxt.as_mut().unwrap().warnings;
for w in warnings.iter_mut().rev() {
if w.source_range == e.source_range {
*w = e;
return;
}
if w.source_range.start() > e.source_range.end() {
break;
}
}
warnings.push(e);
});
}
}
@ -122,11 +142,8 @@ fn expected(what: &'static str) -> StrContext {
fn program(i: TokenSlice) -> PResult<Node<Program>> {
let shebang = opt(shebang).parse_next(i)?;
let mut out: Node<Program> = function_body.parse_next(i)?;
out.shebang = shebang;
// Add the shebang to the non-code meta.
if let Some(shebang) = shebang {
out.non_code_meta.start_nodes.insert(0, shebang);
}
// Match original parser behaviour, for now.
// Once this is merged and stable, consider changing this as I think it's more accurate
// without the -1.
@ -514,7 +531,7 @@ fn whitespace(i: TokenSlice) -> PResult<Vec<Token>> {
/// A shebang is a line at the start of a file that starts with `#!`.
/// If the shebang is present it takes up the whole line.
fn shebang(i: TokenSlice) -> PResult<Node<NonCodeNode>> {
fn shebang(i: TokenSlice) -> PResult<Node<Shebang>> {
// Parse the hash and the bang.
hash.parse_next(i)?;
bang.parse_next(i)?;
@ -537,12 +554,7 @@ fn shebang(i: TokenSlice) -> PResult<Node<NonCodeNode>> {
opt(whitespace).parse_next(i)?;
Ok(Node::new(
NonCodeNode {
value: NonCodeValue::Shebang {
value: format!("#!{}", value),
},
digest: None,
},
Shebang::new(format!("#!{}", value)),
0,
tokens.last().unwrap().end,
tokens.first().unwrap().module_id,
@ -685,7 +697,7 @@ fn object_property(i: TokenSlice) -> PResult<Node<ObjectProperty>> {
let key = identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height = 4', 'height' is the property key")).parse_next(i)?;
ignore_whitespace(i);
// Temporarily accept both `:` and `=` for compatibility.
alt((colon, equals))
let sep = alt((colon, equals))
.context(expected(
"`=`, which separates the property's key from the value you're setting it to, e.g. 'height = 4'",
))
@ -696,7 +708,8 @@ fn object_property(i: TokenSlice) -> PResult<Node<ObjectProperty>> {
"the value which you're setting the property to, e.g. in 'height: 4', the value is 4",
))
.parse_next(i)?;
Ok(Node {
let result = Node {
start: key.start,
end: expr.end(),
module_id: key.module_id,
@ -705,7 +718,18 @@ fn object_property(i: TokenSlice) -> PResult<Node<ObjectProperty>> {
value: expr,
digest: None,
},
})
};
if sep.token_type == TokenType::Colon {
ParseContext::warn(ParseError::with_suggestion(
sep.into(),
Some(result.as_source_range()),
"Using `:` to initialize objects is deprecated, prefer using `=`.",
Some(" ="),
));
}
Ok(result)
}
/// Match something that separates properties of an object.
@ -1048,7 +1072,6 @@ fn noncode_just_after_code(i: TokenSlice) -> PResult<Node<NonCodeNode>> {
// There's an empty line between the body item and the comment,
// This means the comment is a NewLineBlockComment!
let value = match nc.inner.value {
NonCodeValue::Shebang { value } => NonCodeValue::Shebang { value },
// Change block comments to inline, as discussed above
NonCodeValue::BlockComment { value, style } => NonCodeValue::NewLineBlockComment { value, style },
// Other variants don't need to change.
@ -1069,7 +1092,6 @@ fn noncode_just_after_code(i: TokenSlice) -> PResult<Node<NonCodeNode>> {
// There's no newline between the body item and comment,
// so if this is a comment, it must be inline with code.
let value = match nc.inner.value {
NonCodeValue::Shebang { value } => NonCodeValue::Shebang { value },
// Change block comments to inline, as discussed above
NonCodeValue::BlockComment { value, style } => NonCodeValue::InlineComment { value, style },
// Other variants don't need to change.
@ -1269,6 +1291,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Node<Program>> {
Program {
body,
non_code_meta,
shebang: None,
digest: None,
},
start.0,
@ -1919,16 +1942,14 @@ fn double_period(i: TokenSlice) -> PResult<Token> {
.parse_next(i)
}
fn colon(i: TokenSlice) -> PResult<()> {
TokenType::Colon.parse_from(i)?;
Ok(())
fn colon(i: TokenSlice) -> PResult<Token> {
TokenType::Colon.parse_from(i)
}
fn equals(i: TokenSlice) -> PResult<()> {
fn equals(i: TokenSlice) -> PResult<Token> {
one_of((TokenType::Operator, "="))
.context(expected("the equals operator, ="))
.parse_next(i)?;
Ok(())
.parse_next(i)
}
fn question_mark(i: TokenSlice) -> PResult<()> {
@ -2071,57 +2092,84 @@ fn binding_name(i: TokenSlice) -> PResult<Node<Identifier>> {
.parse_next(i)
}
fn typecheck_all(std_fn: Box<dyn StdLibFn>, args: &[Expr]) -> PResult<()> {
// Type check the arguments.
for (i, spec_arg) in std_fn.args(false).iter().enumerate() {
let Some(arg) = &args.get(i) else {
// The executor checks the number of arguments, so we don't need to check it here.
continue;
};
typecheck(spec_arg, arg)?;
}
Ok(())
}
fn typecheck(spec_arg: &crate::docs::StdLibFnArg, arg: &&Expr) -> PResult<()> {
match spec_arg.type_.as_ref() {
"TagNode" => match &arg {
Expr::Identifier(_) => {
// These are fine since we want someone to be able to map a variable to a tag declarator.
}
Expr::TagDeclarator(tag) => {
// TODO: Remove this check. It should be redundant.
tag.clone()
.into_valid_binding_name()
.map_err(|e| ErrMode::Cut(ContextError::from(e)))?;
}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag declarator like `$name`, found {:?}", e),
})
.into(),
));
}
},
"TagIdentifier" => match &arg {
Expr::Identifier(_) => {}
Expr::MemberExpression(_) => {}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag identifier like `tagName`, found {:?}", e),
})
.into(),
));
}
},
_ => {}
}
Ok(())
}
fn fn_call(i: TokenSlice) -> PResult<Node<CallExpression>> {
let fn_name = identifier(i)?;
opt(whitespace).parse_next(i)?;
let _ = terminated(open_paren, opt(whitespace)).parse_next(i)?;
let args = arguments(i)?;
if let Some(std_fn) = crate::std::get_stdlib_fn(&fn_name.name) {
// Type check the arguments.
for (i, spec_arg) in std_fn.args(false).iter().enumerate() {
let Some(arg) = &args.get(i) else {
// The executor checks the number of arguments, so we don't need to check it here.
continue;
};
match spec_arg.type_.as_ref() {
"TagNode" => match &arg {
Expr::Identifier(_) => {
// These are fine since we want someone to be able to map a variable to a tag declarator.
}
Expr::TagDeclarator(tag) => {
// TODO: Remove this check. It should be redundant.
tag.clone()
.into_valid_binding_name()
.map_err(|e| ErrMode::Cut(ContextError::from(e)))?;
}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag declarator like `$name`, found {:?}", e),
})
.into(),
));
}
},
"TagIdentifier" => match &arg {
Expr::Identifier(_) => {}
Expr::MemberExpression(_) => {}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag identifier like `tagName`, found {:?}", e),
})
.into(),
));
}
},
_ => {}
}
}
typecheck_all(std_fn, &args)?;
}
let end = preceded(opt(whitespace), close_paren).parse_next(i)?.end;
// This should really be done with resolved names, but we don't have warning support there
// so we'll hack this in here.
if fn_name.name == "int" {
assert_eq!(args.len(), 1);
let mut arg_str = args[0].recast(&crate::FormatOptions::default(), 0, false);
if arg_str.contains('.') && !arg_str.ends_with(".0") {
arg_str = format!("round({arg_str})");
}
ParseContext::warn(ParseError::with_suggestion(
SourceRange::new(fn_name.start, end, fn_name.module_id),
None,
"`int` function is deprecated. You may not need it at all. If you need to round, consider `round`, `ceil`, or `floor`.",
Some(arg_str),
));
}
Ok(Node {
start: fn_name.start,
end,
@ -2259,7 +2307,7 @@ mod tests {
#[test]
fn test_comments_in_function2() {
let test_program = r#"() => {
const yo = { a: { b: { c: '123' } } } /* block
const yo = { a = { b = { c = '123' } } } /* block
comment */
}"#;
let tokens = crate::token::lexer(test_program, ModuleId::default()).unwrap();
@ -2373,6 +2421,7 @@ const mySk1 = startSketchAt([0, 0])"#;
)],
digest: None,
},
shebang: None,
digest: None,
},
7,
@ -2418,7 +2467,7 @@ const mySk1 = startSketchAt([0, 0])"#;
#[test]
fn many_comments() {
let test_program = r#"// this is a comment
const yo = { a: { b: { c: '123' } } } /* block
const yo = { a = { b = { c = '123' } } } /* block
comment */
const key = 'c'
@ -2455,8 +2504,8 @@ const mySk1 = startSketchAt([0, 0])"#;
},
digest: None,
},
60,
82,
63,
85,
module_id,
),
Node::new(
@ -2464,8 +2513,8 @@ const mySk1 = startSketchAt([0, 0])"#;
value: NonCodeValue::NewLine,
digest: None,
},
82,
86,
85,
89,
module_id,
)
]),
@ -2481,8 +2530,8 @@ const mySk1 = startSketchAt([0, 0])"#;
},
digest: None,
},
103,
129,
106,
132,
module_id,
)]),
non_code_meta.non_code_nodes.get(&1),
@ -2858,7 +2907,7 @@ const mySk1 = startSketchAt([0, 0])"#;
fn test_pipes_on_pipes() {
let test_program = include_str!("../../../tests/executor/inputs/pipes_on_pipes.kcl");
let tokens = crate::token::lexer(test_program, ModuleId::default()).unwrap();
let _actual = program.parse(&tokens).unwrap();
let _ = run_parser(&mut &*tokens).unwrap();
}
#[test]
@ -3110,6 +3159,7 @@ const mySk1 = startSketchAt([0, 0])"#;
4,
module_id,
))],
shebang: None,
non_code_meta: NonCodeMeta::default(),
digest: None,
},
@ -3128,6 +3178,14 @@ const mySk1 = startSketchAt([0, 0])"#;
assert!(result.is_ok());
}
#[track_caller]
fn assert_no_err(p: &str) -> (Node<Program>, ParseContext) {
let result = crate::parser::top_level_parse(p);
let result = result.0.unwrap();
assert!(result.1.errors.is_empty());
(result.0.unwrap(), result.1)
}
#[track_caller]
fn assert_err(p: &str, msg: &str, src: [usize; 2]) {
let result = crate::parser::top_level_parse(p);
@ -3658,6 +3716,28 @@ let myBox = box([0,0], -3, -16, -10)
"#;
assert_err(some_program_string, "Unexpected token: |>", [57, 59]);
}
#[test]
fn warn_object_expr() {
let some_program_string = "{ foo: bar }";
let (_, ctxt) = assert_no_err(some_program_string);
assert_eq!(ctxt.warnings.len(), 1);
assert_eq!(
ctxt.warnings[0].apply_suggestion(some_program_string).unwrap(),
"{ foo = bar }"
)
}
#[test]
fn warn_fn_int() {
let some_program_string = r#"int(1.0)
int(42.3)"#;
let (_, ctxt) = assert_no_err(some_program_string);
assert_eq!(ctxt.warnings.len(), 2);
let replaced = ctxt.warnings[1].apply_suggestion(some_program_string).unwrap();
let replaced = ctxt.warnings[0].apply_suggestion(&replaced).unwrap();
assert_eq!(replaced, "1.0\nround(42.3)");
}
}
#[cfg(test)]
@ -3673,11 +3753,14 @@ mod snapshot_math_tests {
fn $func_name() {
let module_id = crate::ast::types::ModuleId::default();
let tokens = crate::token::lexer($test_kcl_program, module_id).unwrap();
ParseContext::init();
let actual = match binary_expression.parse(&tokens) {
Ok(x) => x,
Err(_e) => panic!("could not parse test"),
};
insta::assert_json_snapshot!(actual);
let _ = ParseContext::take();
}
};
}
@ -3709,6 +3792,7 @@ mod snapshot_tests {
let module_id = crate::ast::types::ModuleId::default();
let tokens = crate::token::lexer($test_kcl_program, module_id).unwrap();
print_tokens(&tokens);
ParseContext::init();
let actual = match program.parse(&tokens) {
Ok(x) => x,
Err(e) => panic!("could not parse test: {e:?}"),
@ -3718,6 +3802,7 @@ mod snapshot_tests {
settings.bind(|| {
insta::assert_json_snapshot!(actual);
});
let _ = ParseContext::take();
}
};
}

View File

@ -24,9 +24,11 @@ pub struct ContextError<C = StrContext> {
#[derive(Debug, Clone)]
pub(crate) struct ParseError {
pub source_range: SourceRange,
#[allow(dead_code)]
pub context_range: Option<SourceRange>,
pub message: String,
#[allow(dead_code)]
pub suggestion: String,
pub suggestion: Option<String>,
pub severity: Severity,
}
@ -34,25 +36,38 @@ impl ParseError {
pub(super) fn err(source_range: SourceRange, message: impl ToString) -> ParseError {
ParseError {
source_range,
context_range: None,
message: message.to_string(),
suggestion: String::new(),
suggestion: None,
severity: Severity::Error,
}
}
#[allow(dead_code)]
pub(super) fn with_suggestion(
source_range: SourceRange,
context_range: Option<SourceRange>,
message: impl ToString,
suggestion: impl ToString,
suggestion: Option<impl ToString>,
) -> ParseError {
ParseError {
source_range,
context_range,
message: message.to_string(),
suggestion: suggestion.to_string(),
suggestion: suggestion.map(|s| s.to_string()),
severity: Severity::Error,
}
}
#[cfg(test)]
pub fn apply_suggestion(&self, src: &str) -> Option<String> {
let suggestion = self.suggestion.as_ref()?;
Some(format!(
"{}{}{}",
&src[0..self.source_range.start()],
suggestion,
&src[self.source_range.end()..]
))
}
}
impl From<ParseError> for KclError {
@ -73,6 +88,8 @@ pub(crate) enum Severity {
/// Helper enum for the below conversion of Winnow errors into either a parse error or an unexpected
/// error.
// TODO we should optimise the size of SourceRange and thus ParseError
#[allow(clippy::large_enum_variant)]
pub(super) enum ErrorKind {
Parse(ParseError),
Internal(KclError),

View File

@ -99,9 +99,30 @@ async fn execute(test_name: &str, render_to_png: bool) {
});
}
Err(e) => {
assert_snapshot(test_name, "Error from executing", || {
insta::assert_snapshot!("execution_error", e);
});
match e {
crate::errors::ExecError::Kcl(error) => {
// Snapshot the KCL error with a fancy graphical report.
// This looks like a Cargo compile error, with arrows pointing
// to source code, underlines, etc.
let report = crate::errors::Report {
error,
filename: format!("{test_name}.kcl"),
kcl_source: read("input.kcl", test_name),
};
let report = miette::Report::new(report);
let report = format!("{:?}", report);
assert_snapshot(test_name, "Error from executing", || {
insta::assert_snapshot!("execution_error", report);
});
}
e => {
// These kinds of errors aren't expected to occur. We don't
// snapshot them because they indicate there's something wrong
// with the Rust test, not with the KCL code being tested.
panic!("{e}")
}
};
}
}
}

View File

@ -1,4 +1,4 @@
use std::{any::type_name, collections::HashMap, num::NonZeroU32};
use std::{any::type_name, num::NonZeroU32};
use anyhow::Result;
use kcmc::{websocket::OkWebSocketResponseData, ModelingCmd};
@ -11,6 +11,7 @@ use crate::{
ExecState, ExecutorContext, ExtrudeSurface, KclValue, Metadata, Sketch, SketchSet, SketchSurface, Solid,
SolidSet, SourceRange, TagIdentifier,
},
kcl_value::KclObjectFields,
std::{shapes::SketchOrSurface, sketch::FaceTag, FnAsArg},
};
@ -1169,7 +1170,7 @@ impl<'a> FromKclValue<'a> for super::sketch::PlaneData {
}
// Case 2: custom plane
let obj = arg.as_object()?;
let_field_of!(obj, plane, &std::collections::HashMap<String, KclValue>);
let_field_of!(obj, plane, &KclObjectFields);
let origin = plane.get("origin").and_then(FromKclValue::from_kcl_val).map(Box::new)?;
let x_axis = plane
.get("xAxis")
@ -1359,7 +1360,7 @@ impl<'a> FromKclValue<'a> for super::revolve::AxisAndOrigin {
}
// Case 2: custom planes.
let obj = arg.as_object()?;
let_field_of!(obj, custom, &HashMap<String, KclValue>);
let_field_of!(obj, custom, &KclObjectFields);
let_field_of!(custom, origin);
let_field_of!(custom, axis);
Some(Self::Custom { axis, origin })
@ -1419,7 +1420,7 @@ impl<'a> FromKclValue<'a> for i64 {
}
impl<'a> FromKclValue<'a> for &'a str {
fn from_kcl_val(arg: &'a KclValue) -> Option<&'a str> {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let KclValue::String { value, meta: _ } = arg else {
return None;
};
@ -1427,8 +1428,8 @@ impl<'a> FromKclValue<'a> for &'a str {
}
}
impl<'a> FromKclValue<'a> for &'a HashMap<String, KclValue> {
fn from_kcl_val(arg: &'a KclValue) -> Option<&'a HashMap<String, KclValue>> {
impl<'a> FromKclValue<'a> for &'a KclObjectFields {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let KclValue::Object { value, meta: _ } = arg else {
return None;
};

View File

@ -1,6 +1,6 @@
//! Standard library patterns.
use std::{cmp::Ordering, collections::HashMap};
use std::cmp::Ordering;
use anyhow::Result;
use derive_docs::stdlib;
@ -22,6 +22,7 @@ use crate::{
ExecState, Geometries, Geometry, KclValue, Point2d, Point3d, Sketch, SketchSet, Solid, SolidSet, SourceRange,
},
function_param::FunctionParam,
kcl_value::KclObjectFields,
std::Args,
};
@ -461,7 +462,7 @@ async fn make_transform<'a, T: GeometryTrait>(
}
fn transform_from_obj_fields<T: GeometryTrait>(
transform: HashMap<String, KclValue>,
transform: KclObjectFields,
source_ranges: Vec<SourceRange>,
) -> Result<Transform, KclError> {
// Apply defaults to the transform.

View File

@ -130,7 +130,7 @@ async fn inner_circle(
},
radius: data.radius,
center: data.center,
ccw: angle_start.to_degrees() < angle_end.to_degrees(),
ccw: angle_start < angle_end,
};
let mut new_sketch = sketch.clone();

View File

@ -1564,7 +1564,7 @@ pub(crate) async fn inner_arc(
source_ranges: vec![args.source_range],
}));
}
let ccw = angle_start.to_degrees() < angle_end.to_degrees();
let ccw = angle_start < angle_end;
let id = exec_state.id_generator.next_uuid();

View File

@ -67,6 +67,8 @@ pub enum TokenType {
Unknown,
/// The ? symbol, used for optional values.
QuestionMark,
/// The @ symbol.
At,
}
/// Most KCL tokens correspond to LSP semantic tokens (but not all).
@ -93,6 +95,7 @@ impl TryFrom<TokenType> for SemanticTokenType {
| TokenType::DoublePeriod
| TokenType::Hash
| TokenType::Dollar
| TokenType::At
| TokenType::Unknown => {
anyhow::bail!("unsupported token type: {:?}", token_type)
}

View File

@ -92,6 +92,7 @@ pub fn token(i: &mut Input<'_>) -> PResult<Token> {
'}' | ')' | ']' => brace_end,
',' => comma,
'?' => question_mark,
'@' => at,
'0'..='9' => number,
':' => colon,
'.' => alt((number, double_period, period)),
@ -268,6 +269,16 @@ fn question_mark(i: &mut Input<'_>) -> PResult<Token> {
))
}
fn at(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = '@'.with_span().parse_next(i)?;
Ok(Token::from_range(
range,
i.state.module_id,
TokenType::At,
value.to_string(),
))
}
fn colon(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = ':'.with_span().parse_next(i)?;
Ok(Token::from_range(

View File

@ -13,6 +13,13 @@ use crate::{
impl Program {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level);
let result = self
.shebang
.as_ref()
.map(|sh| format!("{}\n\n", sh.inner.content))
.unwrap_or_default();
let result = self
.body
.iter()
@ -38,7 +45,7 @@ impl Program {
}
})
.enumerate()
.fold(String::new(), |mut output, (index, recast_str)| {
.fold(result, |mut output, (index, recast_str)| {
let start_string = if index == 0 {
// We need to indent.
if self.non_code_meta.start_nodes.is_empty() {
@ -107,7 +114,7 @@ impl NonCodeValue {
fn should_cause_array_newline(&self) -> bool {
match self {
Self::InlineComment { .. } => false,
Self::Shebang { .. } | Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
}
}
}

View File

@ -1,5 +1,13 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing argument_error.kcl
snapshot_kind: text
---
type: KclErrorDetails { source_ranges: [SourceRange([34, 35, 0])], message: "Expected an array but found Function" }
KCL Type error
× type: Expected an array but found Function
╭─[5:5]
4 │
5 │ map(f, [0, 1])
· ─
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing array_elem_push_fail.kcl
snapshot_kind: text
---
undefined value: KclErrorDetails { source_ranges: [SourceRange([48, 54, 0])], message: "The array doesn't have any item at index 3" }
KCL UndefinedValue error
× undefined value: The array doesn't have any item at index 3
╭─[3:8]
2 │ pushedArr = push(arr, 4)
3 │ fail = arr[3]
· ──────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing array_index_oob.kcl
snapshot_kind: text
---
undefined value: KclErrorDetails { source_ranges: [SourceRange([13, 19, 0])], message: "The array doesn't have any item at index 0" }
KCL UndefinedValue error
× undefined value: The array doesn't have any item at index 0
╭─[2:5]
1 │ arr = []
2 │ x = arr[0]
· ──────
╰────

View File

@ -3,4 +3,10 @@ source: kcl/src/simulation_tests.rs
description: Error from executing comparisons_multiple.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([7, 13, 0])], message: "Expected a number, but found a boolean (true/false value)" }
KCL Semantic error
× semantic: Expected a number, but found a boolean (true/false value)
╭────
1 │ assert(3 == 3 == 3, "this should not compile")
· ──────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing import_constant.kcl
snapshot_kind: text
---
engine: KclErrorDetails { source_ranges: [SourceRange([0, 39, 0])], message: "Failed to read file `export_constant.kcl`: No such file or directory (os error 2)" }
KCL Engine error
× engine: Failed to read file `export_constant.kcl`: No such file or
│ directory (os error 2)
╭────
1 │ import three from "export_constant.kcl"
· ───────────────────────────────────────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing import_cycle1.kcl
snapshot_kind: text
---
engine: KclErrorDetails { source_ranges: [SourceRange([0, 35, 0])], message: "Failed to read file `import_cycle2.kcl`: No such file or directory (os error 2)" }
KCL Engine error
× engine: Failed to read file `import_cycle2.kcl`: No such file or directory
│ (os error 2)
╭─[1:1]
1 │ import two from "import_cycle2.kcl"
· ───────────────────────────────────
2 │
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing import_side_effect.kcl
snapshot_kind: text
---
engine: KclErrorDetails { source_ranges: [SourceRange([0, 40, 0])], message: "Failed to read file `export_side_effect.kcl`: No such file or directory (os error 2)" }
KCL Engine error
× engine: Failed to read file `export_side_effect.kcl`: No such file or
│ directory (os error 2)
╭────
1 │ import foo from "export_side_effect.kcl"
· ────────────────────────────────────────
╰────

View File

@ -1,7 +1,13 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 103
description: Error from executing invalid_index_fractional.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([20, 28, 0])], message: "1.2 is not a valid index, indices must be whole numbers >= 0" }
KCL Semantic error
× semantic: 1.2 is not a valid index, indices must be whole numbers >= 0
╭─[2:5]
1 │ arr = [1, 2, 3]
2 │ x = arr[1.2]
· ────────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing invalid_index_negative.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([27, 33, 0])], message: "'-1' is negative, so you can't index an array with it" }
KCL Semantic error
× semantic: '-1' is negative, so you can't index an array with it
╭─[3:5]
2 │ i = -1
3 │ x = arr[i]
· ──────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing invalid_index_str.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([20, 28, 0])], message: "Only integers >= 0 can be used as the index of an array, but you're using a string" }
KCL Semantic error
× semantic: Only integers >= 0 can be used as the index of an array, but
│ you're using a string
╭─[2:5]
1 │ arr = [1, 2, 3]
2 │ x = arr["s"]
· ────────
╰────

View File

@ -1,7 +1,14 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 103
description: Error from executing invalid_member_object.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([14, 20, 0])], message: "Only arrays and objects can be indexed, but you're trying to index a number" }
KCL Semantic error
× semantic: Only arrays and objects can be indexed, but you're trying to
│ index a number
╭─[2:5]
1 │ num = 999
2 │ x = num[3]
· ──────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing invalid_member_object_prop.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([13, 26, 0])], message: "Only arrays and objects can be indexed, but you're trying to index a boolean (true/false value)" }
KCL Semantic error
× semantic: Only arrays and objects can be indexed, but you're trying to
│ index a boolean (true/false value)
╭─[2:5]
1 │ b = true
2 │ x = b["property"]
· ─────────────
╰────

View File

@ -1,7 +1,14 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 116
description: Error from executing non_string_key_of_object.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([26, 32, 0])], message: "Only strings can be used as the property of an object, but you're using a number" }
KCL Semantic error
× semantic: Only strings can be used as the property of an object, but
│ you're using a number
╭─[2:7]
1 │ obj = { key = 123 }
2 │ num = obj[3]
· ──────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing object_prop_not_found.kcl
snapshot_kind: text
---
undefined value: KclErrorDetails { source_ranges: [SourceRange([15, 25, 0])], message: "Property 'age' not found in object" }
KCL UndefinedValue error
× undefined value: Property 'age' not found in object
╭─[2:5]
1 │ obj = { }
2 │ k = obj["age"]
· ──────────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing pipe_substitution_inside_function_called_from_pipeline.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([106, 107, 0])], message: "cannot use % outside a pipe expression" }
KCL Semantic error
× semantic: cannot use % outside a pipe expression
╭─[6:10]
5 │
6 │ answer = %
· ─
7 │ |> f(%)
╰────

View File

@ -2274,10 +2274,10 @@
"@react-hook/latest" "^1.0.2"
"@react-hook/passive-layout-effect" "^1.2.0"
"@remix-run/router@1.20.0":
version "1.20.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
integrity sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==
"@remix-run/router@1.21.0":
version "1.21.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.21.0.tgz#c65ae4262bdcfe415dbd4f64ec87676e4a56e2b5"
integrity sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==
"@replit/codemirror-interact@^6.3.1":
version "6.3.1"
@ -8126,20 +8126,20 @@ react-refresh@^0.14.2:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
react-router-dom@^6.27.0:
version "6.27.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.27.0.tgz#8d7972a425fd75f91c1e1ff67e47240c5752dc3f"
integrity sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==
react-router-dom@^6.28.0:
version "6.28.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.28.0.tgz#f73ebb3490e59ac9f299377062ad1d10a9f579e6"
integrity sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==
dependencies:
"@remix-run/router" "1.20.0"
react-router "6.27.0"
"@remix-run/router" "1.21.0"
react-router "6.28.0"
react-router@6.27.0:
version "6.27.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.27.0.tgz#db292474926c814c996c0ff3ef0162d1f9f60ed4"
integrity sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==
react-router@6.28.0:
version "6.28.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.28.0.tgz#29247c86d7ba901d7e5a13aa79a96723c3e59d0d"
integrity sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==
dependencies:
"@remix-run/router" "1.20.0"
"@remix-run/router" "1.21.0"
react-textarea-autosize@^8.3.2:
version "8.5.3"
@ -9369,11 +9369,16 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
typescript@^5.0.0, typescript@^5.3.3:
typescript@^5.3.3:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
ua-parser-js@^1.0.35, ua-parser-js@^1.0.37:
version "1.0.38"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2"