Compare commits
24 Commits
derive-doc
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
eeb223a9cb | |||
46be4e7eef | |||
412d1b7a99 | |||
cfdd22af74 | |||
68a11e7aa5 | |||
3139e18dc7 | |||
d461b09a4d | |||
9c42c39ba9 | |||
aa3f40e22c | |||
4423ae16dc | |||
1d45bed649 | |||
64aac7bccc | |||
002edeaf19 | |||
5424252dac | |||
30bc85add8 | |||
39a2bd685b | |||
23a3e330f6 | |||
99dd8b87dc | |||
5ff1d9e268 | |||
ce1a37e0bc | |||
ac605c2434 | |||
f6ecdfcb02 | |||
28815eb2f1 | |||
166fa71f7e |
2
.github/workflows/publish-apps-release.yml
vendored
2
.github/workflows/publish-apps-release.yml
vendored
@ -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: |
|
||||
|
@ -450,3 +450,9 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
## KCL
|
||||
|
||||
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
|
||||
|
||||
### Logging
|
||||
|
||||
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
|
||||
|
||||
To enable memory metrics, build with `--features dhat-heap`.
|
||||
|
@ -58,7 +58,7 @@ mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
|
||||
```js
|
||||
// Sketch on the face of a chamfer.
|
||||
fn cube = (pos, scale) => {
|
||||
fn cube(pos, scale) {
|
||||
sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
|> line([0, scale], %)
|
||||
|
File diff suppressed because one or more lines are too long
@ -37,7 +37,7 @@ assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
|
||||
startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 2 }, %)
|
||||
|> extrude(5, %)
|
||||
|> patternTransform(n, (id) => {
|
||||
|> patternTransform(n, fn(id) {
|
||||
return { translate = [4 * id, 0, 0] }
|
||||
}, %)
|
||||
```
|
||||
|
@ -29,7 +29,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
|
||||
|
||||
```js
|
||||
r = 10 // radius
|
||||
fn drawCircle = (id) => {
|
||||
fn drawCircle(id) {
|
||||
return startSketchOn("XY")
|
||||
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
||||
}
|
||||
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
|
||||
```js
|
||||
r = 10 // radius
|
||||
// Call `map`, using an anonymous function instead of a named one.
|
||||
circles = map([1..3], (id) => {
|
||||
circles = map([1..3], (id) {
|
||||
return startSketchOn("XY")
|
||||
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
||||
})
|
||||
|
File diff suppressed because one or more lines are too long
@ -30,7 +30,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli
|
||||
|
||||
```js
|
||||
// Each instance will be shifted along the X axis.
|
||||
fn transform = (id) => {
|
||||
fn transform(id) {
|
||||
return { translate = [4 * id, 0] }
|
||||
}
|
||||
|
||||
|
@ -30,14 +30,14 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
|
||||
|
||||
```js
|
||||
// This function adds two numbers.
|
||||
fn add = (a, b) => {
|
||||
fn add(a, b) {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// This function adds an array of numbers.
|
||||
// It uses the `reduce` function, to call the `add` function on every
|
||||
// element of the `arr` parameter. The starting value is 0.
|
||||
fn sum = (arr) => {
|
||||
fn sum(arr) {
|
||||
return reduce(arr, 0, add)
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
|
||||
// an anonymous `add` function as its parameter, instead of declaring a
|
||||
// named function outside.
|
||||
arr = [1, 2, 3]
|
||||
sum = reduce(arr, 0, (i, result_so_far) => {
|
||||
sum = reduce(arr, 0, (i, result_so_far) {
|
||||
return i + result_so_far
|
||||
})
|
||||
|
||||
@ -74,7 +74,7 @@ assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
|
||||
|
||||
```js
|
||||
// Declare a function that sketches a decagon.
|
||||
fn decagon = (radius) => {
|
||||
fn decagon(radius) {
|
||||
// Each side of the decagon is turned this many degrees from the previous angle.
|
||||
stepAngle = 1 / 10 * tau()
|
||||
|
||||
@ -84,7 +84,7 @@ fn decagon = (radius) => {
|
||||
// Use a `reduce` to draw the remaining decagon sides.
|
||||
// For each number in the array 1..10, run the given function,
|
||||
// which takes a partially-sketched decagon and adds one more edge to it.
|
||||
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) => {
|
||||
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
|
||||
// Draw one edge of the decagon.
|
||||
x = cos(stepAngle * i) * radius
|
||||
y = sin(stepAngle * i) * radius
|
||||
|
@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|
||||
|> close(%)
|
||||
|> extrude(5, %)
|
||||
|
||||
fn cylinder = (radius, tag) => {
|
||||
fn cylinder(radius, tag) {
|
||||
return startSketchAt([0, 0])
|
||||
|> circle({
|
||||
radius = radius,
|
||||
|
@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|
||||
|> close(%)
|
||||
|> extrude(5, %)
|
||||
|
||||
fn cylinder = (radius, tag) => {
|
||||
fn cylinder(radius, tag) {
|
||||
return startSketchAt([0, 0])
|
||||
|> circle({
|
||||
radius = radius,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,7 @@ If you want to get a value from an array you can use the index like so:
|
||||
An object is defined with `{}` braces. Here is an example object:
|
||||
|
||||
```
|
||||
myObj = {a: 0, b: "thing"}
|
||||
myObj = { a = 0, b = "thing" }
|
||||
```
|
||||
|
||||
We support two different ways of getting properties from objects, you can call
|
||||
@ -90,12 +90,12 @@ startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001, %) - 90,
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001, %),
|
||||
-segLen(rectangleSegmentA001, %)
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
@ -120,18 +120,18 @@ However if the code was written like this:
|
||||
```
|
||||
fn rect = (origin) => {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001, %) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001, %),
|
||||
-segLen(rectangleSegmentA001, %)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
}
|
||||
|
||||
rect([0, 0])
|
||||
@ -148,26 +148,29 @@ For example the following code works.
|
||||
```
|
||||
fn rect = (origin) => {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001, %) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001, %),
|
||||
-segLen(rectangleSegmentA001, %)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
}
|
||||
|
||||
rect([0, 0])
|
||||
myRect = rect([20, 0])
|
||||
|
||||
myRect
|
||||
myRect
|
||||
|> extrude(10, %)
|
||||
|> fillet({radius: 0.5, tags: [myRect.tags.rectangleSegmentA001]}, %)
|
||||
|> fillet({
|
||||
radius = 0.5,
|
||||
tags = [myRect.tags.rectangleSegmentA001]
|
||||
}, %)
|
||||
```
|
||||
|
||||
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside
|
||||
|
@ -85,7 +85,6 @@ layout: manual
|
||||
| `type` |enum: `CallExpression`| | No |
|
||||
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
|
||||
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
|
||||
| `optional` |`boolean`| | 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 |
|
||||
|
@ -125,7 +125,6 @@ An expression can be evaluated to yield a single KCL value.
|
||||
| `type` |enum: `CallExpression`| | No |
|
||||
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No |
|
||||
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
|
||||
| `optional` |`boolean`| | 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 |
|
||||
|
@ -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`
|
||||
|
@ -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
23
docs/kcl/types/Shebang.md
Normal 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 |
|
||||
|
||||
|
@ -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(
|
||||
|
@ -28,6 +28,7 @@ type SceneSerialised = {
|
||||
|
||||
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
|
||||
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
|
||||
type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
|
||||
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
|
||||
type DragFromHandler = (
|
||||
dragParams: mouseDragFromParams
|
||||
@ -68,7 +69,7 @@ export class SceneFixture {
|
||||
x: number,
|
||||
y: number,
|
||||
{ steps }: { steps: number } = { steps: 20 }
|
||||
): [ClickHandler, MoveHandler] =>
|
||||
): [ClickHandler, MoveHandler, DblClickHandler] =>
|
||||
[
|
||||
(clickParams?: mouseParams) => {
|
||||
if (clickParams?.pixelDiff) {
|
||||
@ -90,6 +91,16 @@ export class SceneFixture {
|
||||
}
|
||||
return this.page.mouse.move(x, y, { steps })
|
||||
},
|
||||
(clickParams?: mouseParams) => {
|
||||
if (clickParams?.pixelDiff) {
|
||||
return doAndWaitForImageDiff(
|
||||
this.page,
|
||||
() => this.page.mouse.dblclick(x, y),
|
||||
clickParams.pixelDiff
|
||||
)
|
||||
}
|
||||
return this.page.mouse.dblclick(x, y)
|
||||
},
|
||||
] as const
|
||||
makeDragHelpers = (
|
||||
x: number,
|
||||
|
@ -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')
|
||||
|
@ -551,3 +551,129 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Verify user can double-click to edit a sketch`, async ({
|
||||
app,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
const initialCode = `closedSketch = startSketchOn('XZ')
|
||||
|> circle({ center = [8, 5], radius = 2 }, %)
|
||||
openSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-5, 0], %)
|
||||
|> lineTo([0, 5], %)
|
||||
|> xLine(5, %)
|
||||
|> tangentialArcTo([10, 0], %)
|
||||
`
|
||||
await app.initialise(initialCode)
|
||||
|
||||
const pointInsideCircle = {
|
||||
x: app.viewPortSize.width * 0.63,
|
||||
y: app.viewPortSize.height * 0.5,
|
||||
}
|
||||
const pointOnPathAfterSketching = {
|
||||
x: app.viewPortSize.width * 0.58,
|
||||
y: app.viewPortSize.height * 0.5,
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_clickOpenPath, moveToOpenPath, dblClickOpenPath] =
|
||||
scene.makeMouseHelpers(
|
||||
pointOnPathAfterSketching.x,
|
||||
pointOnPathAfterSketching.y
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_clickCircle, moveToCircle, dblClickCircle] = scene.makeMouseHelpers(
|
||||
pointInsideCircle.x,
|
||||
pointInsideCircle.y
|
||||
)
|
||||
|
||||
const exitSketch = async () => {
|
||||
await test.step(`Exit sketch mode`, async () => {
|
||||
await toolbar.exitSketchBtn.click()
|
||||
await expect(toolbar.exitSketchBtn).not.toBeVisible()
|
||||
await expect(toolbar.startSketchBtn).toBeEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
await test.step(`Double-click on the closed sketch`, async () => {
|
||||
await moveToCircle()
|
||||
await dblClickCircle()
|
||||
await expect(toolbar.startSketchBtn).not.toBeVisible()
|
||||
await expect(toolbar.exitSketchBtn).toBeVisible()
|
||||
await editor.expectState({
|
||||
activeLines: [`|>circle({center=[8,5],radius=2},%)`],
|
||||
highlightedCode: 'circle({center=[8,5],radius=2},%)',
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
|
||||
await exitSketch()
|
||||
|
||||
await test.step(`Double-click on the open sketch`, async () => {
|
||||
await moveToOpenPath()
|
||||
await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
|
||||
// There is a full execution after exiting sketch that clears the scene.
|
||||
await app.page.waitForTimeout(500)
|
||||
await dblClickOpenPath()
|
||||
await expect(toolbar.startSketchBtn).not.toBeVisible()
|
||||
await expect(toolbar.exitSketchBtn).toBeVisible()
|
||||
// Wait for enter sketch mode to complete
|
||||
await app.page.waitForTimeout(500)
|
||||
await editor.expectState({
|
||||
activeLines: [`|>xLine(5,%)`],
|
||||
highlightedCode: 'xLine(5,%)',
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 |
@ -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",
|
||||
|
@ -18,13 +18,13 @@
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.3",
|
||||
"@codemirror/autocomplete": "6.17.0",
|
||||
"@codemirror/language": "^6.10.2",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@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"
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -2,10 +2,10 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@codemirror/autocomplete@^6.16.3":
|
||||
version "6.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz#04d5a4e4e44ccae1ba525d47db53a5479bf46338"
|
||||
integrity sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA==
|
||||
"@codemirror/autocomplete@6.17.0":
|
||||
version "6.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz#24ff5fc37fd91f6439df6f4ff9c8e910cde1b053"
|
||||
integrity sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==
|
||||
dependencies:
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
@ -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"
|
||||
|
@ -5,7 +5,8 @@ export COMMIT=$(git rev-parse --short HEAD)
|
||||
|
||||
# package.json
|
||||
yarn files:set-version
|
||||
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' '.productName=$name' package.json --indent 2)" > package.json
|
||||
PACKAGE=$(jq '.productName="Zoo Modeling App (Nightly)" | .name="zoo-modeling-app-nightly"' package.json --indent 2)
|
||||
echo "$PACKAGE" > package.json
|
||||
|
||||
# electron-builder.yml
|
||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||
import toast from 'react-hot-toast'
|
||||
import { APP_VERSION } from 'routes/Settings'
|
||||
import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
createAndOpenNewTutorialProject,
|
||||
@ -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"
|
||||
@ -264,6 +271,25 @@ export const AllSettingsFields = forwardRef(
|
||||
, and start a discussion if you don't see it! Your feedback will
|
||||
help us prioritize what to build next.
|
||||
</p>
|
||||
{PACKAGE_NAME.indexOf('-nightly') === -1 && (
|
||||
<p className="max-w-2xl mt-6">
|
||||
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"
|
||||
>
|
||||
Click here to grab Zoo Modeling App (Nightly)
|
||||
</a>
|
||||
. It can be installed side-by-side with the stable version
|
||||
you're running now. But careful there, a lot less testing is
|
||||
involved in their release 🤖.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,6 +17,9 @@ import {
|
||||
import { useRouteLoaderData } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
|
||||
enum StreamState {
|
||||
Playing = 'playing',
|
||||
@ -30,6 +33,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 +264,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
|
||||
|
||||
@ -270,12 +282,49 @@ export const Stream = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On double-click of sketch entities we automatically enter sketch mode with the selected sketch,
|
||||
* allowing for quick editing of sketches. TODO: This should be moved to a more central place.
|
||||
*/
|
||||
const enterSketchModeIfSelectingSketch: MouseEventHandler<HTMLDivElement> = (
|
||||
e
|
||||
) => {
|
||||
if (
|
||||
!isNetworkOkay ||
|
||||
!videoRef.current ||
|
||||
state.matches('Sketch') ||
|
||||
state.matches({ idle: 'showPlanes' }) ||
|
||||
sceneInfra.camControls.wasDragging === true ||
|
||||
!btnName(e.nativeEvent).left
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
sendSelectEventToEngine(e, videoRef.current)
|
||||
.then(({ entity_id }) => {
|
||||
if (!entity_id) {
|
||||
// No entity selected. This is benign
|
||||
return
|
||||
}
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: entity_id, types: ['path', 'solid2D', 'segment'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(path)) {
|
||||
return path
|
||||
}
|
||||
sceneInfra.modelingSend({ type: 'Enter sketch' })
|
||||
})
|
||||
.catch(reportRejection)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
id="stream"
|
||||
data-testid="stream"
|
||||
onClick={handleMouseUp}
|
||||
onDoubleClick={enterSketchModeIfSelectingSketch}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
>
|
||||
|
@ -169,6 +169,7 @@ export function useEngineConnectionSubscriptions() {
|
||||
pathToNode: artifact.codeRef.pathToNode,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Artifact is likely an extrusion face
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -384,7 +384,6 @@ const myVar = funcN(1, 2)`
|
||||
raw: '2',
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -465,7 +464,6 @@ describe('testing pipe operator special', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
type: 'CallExpression',
|
||||
@ -508,7 +506,6 @@ describe('testing pipe operator special', () => {
|
||||
end: 60,
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
type: 'CallExpression',
|
||||
@ -556,7 +553,6 @@ describe('testing pipe operator special', () => {
|
||||
value: 'myPath',
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
type: 'CallExpression',
|
||||
@ -598,7 +594,6 @@ describe('testing pipe operator special', () => {
|
||||
end: 115,
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
type: 'CallExpression',
|
||||
@ -625,7 +620,6 @@ describe('testing pipe operator special', () => {
|
||||
end: 130,
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -711,7 +705,6 @@ describe('testing pipe operator special', () => {
|
||||
end: 35,
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -1765,7 +1758,6 @@ describe('test UnaryExpression', () => {
|
||||
raw: '100',
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -1837,11 +1829,9 @@ describe('testing nested call expressions', () => {
|
||||
raw: '3',
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1879,7 +1869,6 @@ describe('should recognise callExpresions in binaryExpressions', () => {
|
||||
name: 'seg02',
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
},
|
||||
right: {
|
||||
type: 'Literal',
|
||||
|
@ -123,6 +123,7 @@ describe('Testing addSketchTo', () => {
|
||||
const result = addSketchTo(
|
||||
{
|
||||
body: [],
|
||||
shebang: null,
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
@ -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
|
||||
@ -688,7 +727,6 @@ export function createCallExpressionStdLib(
|
||||
|
||||
name,
|
||||
},
|
||||
optional: false,
|
||||
arguments: args,
|
||||
}
|
||||
}
|
||||
@ -710,7 +748,6 @@ export function createCallExpression(
|
||||
|
||||
name,
|
||||
},
|
||||
optional: false,
|
||||
arguments: args,
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ log(5, myVar)
|
||||
})
|
||||
it('function declaration with call', () => {
|
||||
const code = [
|
||||
'fn funcN = (a, b) => {',
|
||||
'fn funcN(a, b) {',
|
||||
' return a + b',
|
||||
'}',
|
||||
'theVar = 60',
|
||||
@ -101,7 +101,7 @@ log(5, myVar)
|
||||
})
|
||||
it('recast BinaryExpression piped into CallExpression', () => {
|
||||
const code = [
|
||||
'fn myFn = (a) => {',
|
||||
'fn myFn(a) {',
|
||||
' return a + 1',
|
||||
'}',
|
||||
'myVar = 5 + 1',
|
||||
@ -245,7 +245,7 @@ key = 'c'
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('comments in a fn block', () => {
|
||||
const code = `fn myFn = () => {
|
||||
const code = `fn myFn() {
|
||||
// this is a comment
|
||||
yo = { a = { b = { c = '123' } } }
|
||||
|
||||
|
@ -1823,6 +1823,7 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
||||
modifiedAst: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
shebang: null,
|
||||
moduleId: 0,
|
||||
body: [],
|
||||
|
||||
|
@ -1,382 +0,0 @@
|
||||
import { lexer, initPromise } from './wasm'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
})
|
||||
|
||||
describe('testing lexer', () => {
|
||||
it('async lexer works too', async () => {
|
||||
const code = '1 + 2'
|
||||
const code2 = `const yo = {key: 'value'}`
|
||||
const code3 = `const yo = 45 /* this is a comment
|
||||
const ya = 6 */
|
||||
const yi=45`
|
||||
expect(lexer(code)).toEqual(lexer(code))
|
||||
expect(lexer(code2)).toEqual(lexer(code2))
|
||||
expect(lexer(code3)).toEqual(lexer(code3))
|
||||
})
|
||||
it('test lexer', () => {
|
||||
expect(stringSummaryLexer('1 + 2')).toEqual([
|
||||
"number '1' from 0 to 1",
|
||||
"whitespace ' ' from 1 to 3",
|
||||
"operator '+' from 3 to 4",
|
||||
"whitespace ' ' from 4 to 5",
|
||||
"number '2' from 5 to 6",
|
||||
])
|
||||
expect(stringSummaryLexer('54 + 22500 + 6')).toEqual([
|
||||
"number '54' from 0 to 2",
|
||||
"whitespace ' ' from 2 to 3",
|
||||
"operator '+' from 3 to 4",
|
||||
"whitespace ' ' from 4 to 5",
|
||||
"number '22500' from 5 to 10",
|
||||
"whitespace ' ' from 10 to 11",
|
||||
"operator '+' from 11 to 12",
|
||||
"whitespace ' ' from 12 to 13",
|
||||
"number '6' from 13 to 14",
|
||||
])
|
||||
expect(stringSummaryLexer('a + bo + t5 - 6')).toEqual([
|
||||
"word 'a' from 0 to 1",
|
||||
"whitespace ' ' from 1 to 2",
|
||||
"operator '+' from 2 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"word 'bo' from 4 to 6",
|
||||
"whitespace ' ' from 6 to 7",
|
||||
"operator '+' from 7 to 8",
|
||||
"whitespace ' ' from 8 to 9",
|
||||
"word 't5' from 9 to 11",
|
||||
"whitespace ' ' from 11 to 12",
|
||||
"operator '-' from 12 to 13",
|
||||
"whitespace ' ' from 13 to 14",
|
||||
"number '6' from 14 to 15",
|
||||
])
|
||||
expect(stringSummaryLexer('a + "a str" - 6')).toEqual([
|
||||
"word 'a' from 0 to 1",
|
||||
"whitespace ' ' from 1 to 2",
|
||||
"operator '+' from 2 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
'string \'"a str"\' from 4 to 11',
|
||||
"whitespace ' ' from 11 to 12",
|
||||
"operator '-' from 12 to 13",
|
||||
"whitespace ' ' from 13 to 14",
|
||||
"number '6' from 14 to 15",
|
||||
])
|
||||
expect(stringSummaryLexer("a + 'str'")).toEqual([
|
||||
"word 'a' from 0 to 1",
|
||||
"whitespace ' ' from 1 to 2",
|
||||
"operator '+' from 2 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"string ''str'' from 4 to 9",
|
||||
])
|
||||
expect(stringSummaryLexer("a +'str'")).toEqual([
|
||||
"word 'a' from 0 to 1",
|
||||
"whitespace ' ' from 1 to 2",
|
||||
"operator '+' from 2 to 3",
|
||||
"string ''str'' from 3 to 8",
|
||||
])
|
||||
|
||||
expect(stringSummaryLexer('a + (sick)')).toEqual([
|
||||
"word 'a' from 0 to 1",
|
||||
"whitespace ' ' from 1 to 2",
|
||||
"operator '+' from 2 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"brace '(' from 4 to 5",
|
||||
"word 'sick' from 5 to 9",
|
||||
"brace ')' from 9 to 10",
|
||||
])
|
||||
|
||||
expect(stringSummaryLexer('a + { sick}')).toEqual([
|
||||
"word 'a' from 0 to 1",
|
||||
"whitespace ' ' from 1 to 2",
|
||||
"operator '+' from 2 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"brace '{' from 4 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"word 'sick' from 6 to 10",
|
||||
"brace '}' from 10 to 11",
|
||||
])
|
||||
|
||||
expect(stringSummaryLexer("log('hi')")).toEqual([
|
||||
"word 'log' from 0 to 3",
|
||||
"brace '(' from 3 to 4",
|
||||
"string ''hi'' from 4 to 8",
|
||||
"brace ')' from 8 to 9",
|
||||
])
|
||||
expect(stringSummaryLexer("log('hi', 'hello')")).toEqual([
|
||||
"word 'log' from 0 to 3",
|
||||
"brace '(' from 3 to 4",
|
||||
"string ''hi'' from 4 to 8",
|
||||
"comma ',' from 8 to 9",
|
||||
"whitespace ' ' from 9 to 10",
|
||||
"string ''hello'' from 10 to 17",
|
||||
"brace ')' from 17 to 18",
|
||||
])
|
||||
expect(stringSummaryLexer('fn funcName = (param1, param2) => {}')).toEqual([
|
||||
"keyword 'fn' from 0 to 2",
|
||||
"whitespace ' ' from 2 to 3",
|
||||
"word 'funcName' from 3 to 11",
|
||||
"whitespace ' ' from 11 to 12",
|
||||
"operator '=' from 12 to 13",
|
||||
"whitespace ' ' from 13 to 14",
|
||||
"brace '(' from 14 to 15",
|
||||
"word 'param1' from 15 to 21",
|
||||
"comma ',' from 21 to 22",
|
||||
"whitespace ' ' from 22 to 23",
|
||||
"word 'param2' from 23 to 29",
|
||||
"brace ')' from 29 to 30",
|
||||
"whitespace ' ' from 30 to 31",
|
||||
"operator '=>' from 31 to 33",
|
||||
"whitespace ' ' from 33 to 34",
|
||||
"brace '{' from 34 to 35",
|
||||
"brace '}' from 35 to 36",
|
||||
])
|
||||
})
|
||||
it('test negative and decimal numbers', () => {
|
||||
expect(stringSummaryLexer('-1')).toEqual([
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1' from 1 to 2",
|
||||
])
|
||||
expect(stringSummaryLexer('-1.5')).toEqual([
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1.5' from 1 to 4",
|
||||
])
|
||||
expect(stringSummaryLexer('1.5')).toEqual([
|
||||
"number '1.5' from 0 to 3",
|
||||
])
|
||||
expect(stringSummaryLexer('1.5 + 2.5')).toEqual([
|
||||
"number '1.5' from 0 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"operator '+' from 4 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"number '2.5' from 6 to 9",
|
||||
])
|
||||
expect(stringSummaryLexer('1.5 - 2.5')).toEqual([
|
||||
"number '1.5' from 0 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"operator '-' from 4 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"number '2.5' from 6 to 9",
|
||||
])
|
||||
expect(stringSummaryLexer('1.5 + -2.5')).toEqual([
|
||||
"number '1.5' from 0 to 3",
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"operator '+' from 4 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"operator '-' from 6 to 7",
|
||||
"number '2.5' from 7 to 10",
|
||||
])
|
||||
expect(stringSummaryLexer('-1.5 + 2.5')).toEqual([
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1.5' from 1 to 4",
|
||||
"whitespace ' ' from 4 to 5",
|
||||
"operator '+' from 5 to 6",
|
||||
"whitespace ' ' from 6 to 7",
|
||||
"number '2.5' from 7 to 10",
|
||||
])
|
||||
})
|
||||
it('testing piping operator', () => {
|
||||
const result = stringSummaryLexer(`sketch mySketch {
|
||||
lineTo(2, 3)
|
||||
} |> rx(45, %)`)
|
||||
expect(result).toEqual([
|
||||
"type 'sketch' from 0 to 6",
|
||||
"whitespace ' ' from 6 to 7",
|
||||
"word 'mySketch' from 7 to 15",
|
||||
"whitespace ' ' from 15 to 16",
|
||||
"brace '{' from 16 to 17",
|
||||
"whitespace '\n ' from 17 to 24",
|
||||
"word 'lineTo' from 24 to 30",
|
||||
"brace '(' from 30 to 31",
|
||||
"number '2' from 31 to 32",
|
||||
"comma ',' from 32 to 33",
|
||||
"whitespace ' ' from 33 to 34",
|
||||
"number '3' from 34 to 35",
|
||||
"brace ')' from 35 to 36",
|
||||
"whitespace '\n ' from 36 to 41",
|
||||
"brace '}' from 41 to 42",
|
||||
"whitespace ' ' from 42 to 43",
|
||||
"operator '|>' from 43 to 45",
|
||||
"whitespace ' ' from 45 to 46",
|
||||
"word 'rx' from 46 to 48",
|
||||
"brace '(' from 48 to 49",
|
||||
"number '45' from 49 to 51",
|
||||
"comma ',' from 51 to 52",
|
||||
"whitespace ' ' from 52 to 53",
|
||||
"operator '%' from 53 to 54",
|
||||
"brace ')' from 54 to 55",
|
||||
])
|
||||
})
|
||||
it('testing array declaration', () => {
|
||||
const result = stringSummaryLexer(`const yo = [1, 2]`)
|
||||
expect(result).toEqual([
|
||||
"keyword 'const' from 0 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"word 'yo' from 6 to 8",
|
||||
"whitespace ' ' from 8 to 9",
|
||||
"operator '=' from 9 to 10",
|
||||
"whitespace ' ' from 10 to 11",
|
||||
"brace '[' from 11 to 12",
|
||||
"number '1' from 12 to 13",
|
||||
"comma ',' from 13 to 14",
|
||||
"whitespace ' ' from 14 to 15",
|
||||
"number '2' from 15 to 16",
|
||||
"brace ']' from 16 to 17",
|
||||
])
|
||||
})
|
||||
it('testing object declaration', () => {
|
||||
const result = stringSummaryLexer(`const yo = {key: 'value'}`)
|
||||
expect(result).toEqual([
|
||||
"keyword 'const' from 0 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"word 'yo' from 6 to 8",
|
||||
"whitespace ' ' from 8 to 9",
|
||||
"operator '=' from 9 to 10",
|
||||
"whitespace ' ' from 10 to 11",
|
||||
"brace '{' from 11 to 12",
|
||||
"word 'key' from 12 to 15",
|
||||
"colon ':' from 15 to 16",
|
||||
"whitespace ' ' from 16 to 17",
|
||||
"string ''value'' from 17 to 24",
|
||||
"brace '}' from 24 to 25",
|
||||
])
|
||||
})
|
||||
it('testing object property access', () => {
|
||||
const result = stringSummaryLexer(`const yo = {key: 'value'}
|
||||
const prop = yo.key
|
||||
const prop2 = yo['key']
|
||||
const key = 'key'
|
||||
const prop3 = yo[key]`)
|
||||
expect(result).toEqual([
|
||||
"keyword 'const' from 0 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"word 'yo' from 6 to 8",
|
||||
"whitespace ' ' from 8 to 9",
|
||||
"operator '=' from 9 to 10",
|
||||
"whitespace ' ' from 10 to 11",
|
||||
"brace '{' from 11 to 12",
|
||||
"word 'key' from 12 to 15",
|
||||
"colon ':' from 15 to 16",
|
||||
"whitespace ' ' from 16 to 17",
|
||||
"string ''value'' from 17 to 24",
|
||||
"brace '}' from 24 to 25",
|
||||
"whitespace '\n' from 25 to 26",
|
||||
"keyword 'const' from 26 to 31",
|
||||
"whitespace ' ' from 31 to 32",
|
||||
"word 'prop' from 32 to 36",
|
||||
"whitespace ' ' from 36 to 37",
|
||||
"operator '=' from 37 to 38",
|
||||
"whitespace ' ' from 38 to 39",
|
||||
"word 'yo' from 39 to 41",
|
||||
"period '.' from 41 to 42",
|
||||
"word 'key' from 42 to 45",
|
||||
"whitespace '\n' from 45 to 46",
|
||||
"keyword 'const' from 46 to 51",
|
||||
"whitespace ' ' from 51 to 52",
|
||||
"word 'prop2' from 52 to 57",
|
||||
"whitespace ' ' from 57 to 58",
|
||||
"operator '=' from 58 to 59",
|
||||
"whitespace ' ' from 59 to 60",
|
||||
"word 'yo' from 60 to 62",
|
||||
"brace '[' from 62 to 63",
|
||||
"string ''key'' from 63 to 68",
|
||||
"brace ']' from 68 to 69",
|
||||
"whitespace '\n' from 69 to 70",
|
||||
"keyword 'const' from 70 to 75",
|
||||
"whitespace ' ' from 75 to 76",
|
||||
"word 'key' from 76 to 79",
|
||||
"whitespace ' ' from 79 to 80",
|
||||
"operator '=' from 80 to 81",
|
||||
"whitespace ' ' from 81 to 82",
|
||||
"string ''key'' from 82 to 87",
|
||||
"whitespace '\n' from 87 to 88",
|
||||
"keyword 'const' from 88 to 93",
|
||||
"whitespace ' ' from 93 to 94",
|
||||
"word 'prop3' from 94 to 99",
|
||||
"whitespace ' ' from 99 to 100",
|
||||
"operator '=' from 100 to 101",
|
||||
"whitespace ' ' from 101 to 102",
|
||||
"word 'yo' from 102 to 104",
|
||||
"brace '[' from 104 to 105",
|
||||
"word 'key' from 105 to 108",
|
||||
"brace ']' from 108 to 109",
|
||||
])
|
||||
})
|
||||
it('testing tokenising line comments', () => {
|
||||
const result = stringSummaryLexer(`const yo = 45 // this is a comment
|
||||
const yo = 6`)
|
||||
expect(result).toEqual([
|
||||
"keyword 'const' from 0 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"word 'yo' from 6 to 8",
|
||||
"whitespace ' ' from 8 to 9",
|
||||
"operator '=' from 9 to 10",
|
||||
"whitespace ' ' from 10 to 11",
|
||||
"number '45' from 11 to 13",
|
||||
"whitespace ' ' from 13 to 14",
|
||||
"lineComment '// this is a comment' from 14 to 34",
|
||||
"whitespace '\n' from 34 to 35",
|
||||
"keyword 'const' from 35 to 40",
|
||||
"whitespace ' ' from 40 to 41",
|
||||
"word 'yo' from 41 to 43",
|
||||
"whitespace ' ' from 43 to 44",
|
||||
"operator '=' from 44 to 45",
|
||||
"whitespace ' ' from 45 to 46",
|
||||
"number '6' from 46 to 47",
|
||||
])
|
||||
})
|
||||
it('testing tokenising line comments by itself', () => {
|
||||
const result = stringSummaryLexer(`log('hi')
|
||||
// comment on a line by itself
|
||||
const yo=45`)
|
||||
expect(result).toEqual([
|
||||
"word 'log' from 0 to 3",
|
||||
"brace '(' from 3 to 4",
|
||||
"string ''hi'' from 4 to 8",
|
||||
"brace ')' from 8 to 9",
|
||||
"whitespace '\n' from 9 to 10",
|
||||
"lineComment '// comment on a line by itself' from 10 to 40",
|
||||
"whitespace '\n' from 40 to 41",
|
||||
"keyword 'const' from 41 to 46",
|
||||
"whitespace ' ' from 46 to 47",
|
||||
"word 'yo' from 47 to 49",
|
||||
"operator '=' from 49 to 50",
|
||||
"number '45' from 50 to 52",
|
||||
])
|
||||
})
|
||||
it('testing tokenising block comments', () => {
|
||||
const result = stringSummaryLexer(`const yo = 45 /* this is a comment
|
||||
const ya = 6 */
|
||||
const yi=45`)
|
||||
expect(result).toEqual([
|
||||
"keyword 'const' from 0 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"word 'yo' from 6 to 8",
|
||||
"whitespace ' ' from 8 to 9",
|
||||
"operator '=' from 9 to 10",
|
||||
"whitespace ' ' from 10 to 11",
|
||||
"number '45' from 11 to 13",
|
||||
"whitespace ' ' from 13 to 14",
|
||||
`blockComment '/* this is a comment
|
||||
const ya = 6 */' from 14 to 50`,
|
||||
"whitespace '\n' from 50 to 51",
|
||||
"keyword 'const' from 51 to 56",
|
||||
"whitespace ' ' from 56 to 57",
|
||||
"word 'yi' from 57 to 59",
|
||||
"operator '=' from 59 to 60",
|
||||
"number '45' from 60 to 62",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// helpers
|
||||
|
||||
const stringSummaryLexer = (input: string) => {
|
||||
const tokens = lexer(input)
|
||||
if (err(tokens)) return []
|
||||
return tokens.map(
|
||||
({ type, value, start, end }) =>
|
||||
`${type.padEnd(12, ' ')} ${`'${value}'`.padEnd(10, ' ')} from ${String(
|
||||
start
|
||||
).padEnd(3, ' ')} to ${end}`
|
||||
)
|
||||
}
|
@ -3,7 +3,6 @@ import init, {
|
||||
recast_wasm,
|
||||
execute_wasm,
|
||||
kcl_lint,
|
||||
lexer_wasm,
|
||||
modify_ast_for_sketch_wasm,
|
||||
is_points_ccw,
|
||||
get_tangential_arc_to_info,
|
||||
@ -24,7 +23,6 @@ import { EngineCommandManager } from './std/engineConnection'
|
||||
import { Discovered } from '../wasm-lib/kcl/bindings/Discovered'
|
||||
import { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
|
||||
import type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
import type { Token } from '../wasm-lib/kcl/bindings/Token'
|
||||
import { Coords2d } from './std/sketch'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import { CoreDumpInfo } from 'wasm-lib/kcl/bindings/CoreDumpInfo'
|
||||
@ -507,10 +505,6 @@ export const modifyGrid = async (
|
||||
}
|
||||
}
|
||||
|
||||
export function lexer(str: string): Token[] | Error {
|
||||
return lexer_wasm(str)
|
||||
}
|
||||
|
||||
export const modifyAstForSketch = async (
|
||||
engineCommandManager: EngineCommandManager,
|
||||
ast: Node<Program>,
|
||||
|
@ -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',
|
||||
|
@ -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`
|
||||
|
@ -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 &&
|
||||
|
@ -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: [],
|
||||
|
@ -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
@ -26,6 +26,10 @@ export const APP_VERSION =
|
||||
window.electron.packageJson.version
|
||||
: 'main'
|
||||
|
||||
export const PACKAGE_NAME = isDesktop()
|
||||
? window.electron.packageJson.name
|
||||
: 'zoo-modeling-app'
|
||||
|
||||
export const Settings = () => {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
157
src/wasm-lib/Cargo.lock
generated
157
src/wasm-lib/Cargo.lock
generated
@ -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"
|
||||
@ -765,6 +774,22 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dhat"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"lazy_static",
|
||||
"mintex",
|
||||
"parking_lot 0.12.3",
|
||||
"rustc-hash 1.1.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thousands",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@ -1625,6 +1650,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"
|
||||
@ -1675,7 +1706,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.25"
|
||||
version = "0.2.26"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1690,6 +1721,7 @@ dependencies = [
|
||||
"dashmap 6.1.0",
|
||||
"databake",
|
||||
"derive-docs",
|
||||
"dhat",
|
||||
"expectorate",
|
||||
"fnv",
|
||||
"form_urlencoded",
|
||||
@ -1708,6 +1740,7 @@ dependencies = [
|
||||
"kittycad-modeling-cmds",
|
||||
"lazy_static",
|
||||
"measurements",
|
||||
"miette",
|
||||
"mime_guess",
|
||||
"parse-display 0.9.1",
|
||||
"pretty_assertions",
|
||||
@ -1733,6 +1766,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"web-time",
|
||||
"winnow",
|
||||
"zip",
|
||||
]
|
||||
@ -1806,9 +1840,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds"
|
||||
version = "0.2.76"
|
||||
version = "0.2.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2394fe2b28a1c6bd524dec1dbcd7e839c2782a6ecc743085e122cde77ee19cfa"
|
||||
checksum = "3b77259b37acafa360d98af27431ac394bc8899eeed7037513832ddbee856811"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -1971,6 +2005,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"
|
||||
@ -2009,6 +2074,12 @@ version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
|
||||
|
||||
[[package]]
|
||||
name = "mintex"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bec4598fddb13cc7b528819e697852653252b760f1228b7642679bf2ff2cd07"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.2"
|
||||
@ -2163,6 +2234,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"
|
||||
@ -2591,7 +2668,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 1.0.68",
|
||||
@ -2608,7 +2685,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls",
|
||||
"slab",
|
||||
"thiserror 1.0.68",
|
||||
@ -2948,6 +3025,12 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.0.0"
|
||||
@ -3311,6 +3394,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 +3485,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 +3606,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"
|
||||
@ -3536,6 +3667,12 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thousands"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
@ -3614,9 +3751,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 +4092,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"
|
||||
|
@ -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"] }
|
||||
|
||||
|
@ -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"] }
|
||||
|
@ -203,7 +203,7 @@ fn bad_gateway(msg: String) -> Response<Body> {
|
||||
resp
|
||||
}
|
||||
|
||||
fn kcl_err(err: anyhow::Error) -> Response<Body> {
|
||||
fn kcl_err(err: impl std::fmt::Display) -> Response<Body> {
|
||||
eprintln!("\tBad KCL");
|
||||
bad_gateway(format!("{err}"))
|
||||
}
|
||||
|
@ -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"] }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.25"
|
||||
version = "0.2.26"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -21,6 +21,7 @@ convert_case = "0.6.0"
|
||||
dashmap = "6.1.0"
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.29", path = "../derive-docs" }
|
||||
dhat = { version = "0.3", optional = true }
|
||||
fnv = "1.0.7"
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.31" }
|
||||
@ -33,6 +34,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 }
|
||||
@ -50,12 +52,13 @@ url = { version = "2.5.3", features = ["serde"] }
|
||||
urlencoding = "2.1.3"
|
||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
||||
validator = { version = "0.19.0", features = ["derive"] }
|
||||
web-time = "1.1"
|
||||
winnow = "0.6.18"
|
||||
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,13 +67,14 @@ 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"] }
|
||||
|
||||
[features]
|
||||
default = ["engine"]
|
||||
cli = ["dep:clap"]
|
||||
dhat-heap = ["dep:dhat"]
|
||||
# For the lsp server, when run with stdout for rpc we want to disable println.
|
||||
# This is used for editor extensions that use the lsp server.
|
||||
disable-println = []
|
||||
@ -89,8 +93,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]]
|
||||
|
@ -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)]
|
||||
@ -619,7 +644,7 @@ pub enum Expr {
|
||||
|
||||
impl Expr {
|
||||
pub fn get_lsp_folding_range(&self) -> Option<FoldingRange> {
|
||||
let recasted = self.recast(&FormatOptions::default(), 0, false);
|
||||
let recasted = self.recast(&FormatOptions::default(), 0, crate::unparser::ExprContext::Other);
|
||||
// If the code only has one line then we don't need to fold it.
|
||||
if recasted.lines().count() <= 1 {
|
||||
return None;
|
||||
@ -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`.
|
||||
@ -1254,7 +1264,6 @@ pub struct ExpressionStatement {
|
||||
pub struct CallExpression {
|
||||
pub callee: Node<Identifier>,
|
||||
pub arguments: Vec<Expr>,
|
||||
pub optional: bool,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@ -1291,7 +1300,6 @@ impl CallExpression {
|
||||
Ok(Node::no_src(Self {
|
||||
callee: Identifier::new(name),
|
||||
arguments,
|
||||
optional: false,
|
||||
digest: None,
|
||||
}))
|
||||
}
|
||||
@ -3059,9 +3067,9 @@ ghi("things")
|
||||
folding_ranges[1].collapsed_text,
|
||||
Some("startSketchOn('XY')".to_string())
|
||||
);
|
||||
assert_eq!(folding_ranges[2].start_line, 390);
|
||||
assert_eq!(folding_ranges[2].start_line, 384);
|
||||
assert_eq!(folding_ranges[2].end_line, 403);
|
||||
assert_eq!(folding_ranges[2].collapsed_text, Some("fn ghi = (x) => {".to_string()));
|
||||
assert_eq!(folding_ranges[2].collapsed_text, Some("fn ghi(x) {".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -3260,7 +3268,7 @@ const cylinder = startSketchOn('-XZ')
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_parse_return_type_on_functions() {
|
||||
let some_program_string = r#"fn thing = () => {thing: number, things: string[], more?: string} {
|
||||
let some_program_string = r#"fn thing(): {thing: number, things: string[], more?: string} {
|
||||
return 1
|
||||
}"#;
|
||||
let module_id = ModuleId::default();
|
||||
@ -3286,8 +3294,8 @@ const cylinder = startSketchOn('-XZ')
|
||||
name: "thing".to_owned(),
|
||||
digest: None
|
||||
},
|
||||
13,
|
||||
18,
|
||||
23,
|
||||
module_id,
|
||||
),
|
||||
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number)),
|
||||
@ -3300,8 +3308,8 @@ const cylinder = startSketchOn('-XZ')
|
||||
name: "things".to_owned(),
|
||||
digest: None
|
||||
},
|
||||
33,
|
||||
39,
|
||||
28,
|
||||
34,
|
||||
module_id,
|
||||
),
|
||||
type_: Some(FnArgType::Array(FnArgPrimitive::String)),
|
||||
@ -3314,8 +3322,8 @@ const cylinder = startSketchOn('-XZ')
|
||||
name: "more".to_owned(),
|
||||
digest: None
|
||||
},
|
||||
51,
|
||||
55,
|
||||
46,
|
||||
50,
|
||||
module_id,
|
||||
),
|
||||
type_: Some(FnArgType::Primitive(FnArgPrimitive::String)),
|
||||
@ -3338,6 +3346,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 +3370,7 @@ const cylinder = startSketchOn('-XZ')
|
||||
inner: Program {
|
||||
body: Vec::new(),
|
||||
non_code_meta: Default::default(),
|
||||
shebang: None,
|
||||
digest: None,
|
||||
},
|
||||
start: 0,
|
||||
@ -3388,6 +3398,7 @@ const cylinder = startSketchOn('-XZ')
|
||||
inner: Program {
|
||||
body: Vec::new(),
|
||||
non_code_meta: Default::default(),
|
||||
shebang: None,
|
||||
digest: None,
|
||||
},
|
||||
start: 0,
|
||||
@ -3426,6 +3437,7 @@ const cylinder = startSketchOn('-XZ')
|
||||
inner: Program {
|
||||
body: Vec::new(),
|
||||
non_code_meta: Default::default(),
|
||||
shebang: None,
|
||||
digest: None,
|
||||
},
|
||||
start: 0,
|
||||
|
@ -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());
|
||||
@ -369,7 +369,6 @@ impl CallExpression {
|
||||
for argument in slf.arguments.iter_mut() {
|
||||
hasher.update(argument.compute_digest());
|
||||
}
|
||||
hasher.update(if slf.optional { [1] } else { [0] });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -282,8 +282,8 @@ impl EngineConnection {
|
||||
}
|
||||
Err(e) => {
|
||||
match &e {
|
||||
WebSocketReadError::Read(e) => eprintln!("could not read from WS: {:?}", e),
|
||||
WebSocketReadError::Deser(e) => eprintln!("could not deserialize msg from WS: {:?}", e),
|
||||
WebSocketReadError::Read(e) => crate::logln!("could not read from WS: {:?}", e),
|
||||
WebSocketReadError::Deser(e) => crate::logln!("could not deserialize msg from WS: {:?}", e),
|
||||
}
|
||||
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive;
|
||||
return Err(e);
|
||||
|
@ -4,6 +4,26 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
use crate::{ast::types::ModuleId, executor::SourceRange, lsp::IntoDiagnostic};
|
||||
|
||||
/// How did the KCL execution fail
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ExecError {
|
||||
#[error("{0}")]
|
||||
Kcl(#[from] crate::KclError),
|
||||
#[error("Could not connect to engine: {0}")]
|
||||
Connection(#[from] ConnectionError),
|
||||
#[error("PNG snapshot could not be decoded: {0}")]
|
||||
BadPng(String),
|
||||
}
|
||||
|
||||
/// How did KCL client fail to connect to the engine
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ConnectionError {
|
||||
#[error("Could not create a Zoo client: {0}")]
|
||||
CouldNotMakeClient(anyhow::Error),
|
||||
#[error("Could not establish connection to engine: {0}")]
|
||||
Establishing(anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
@ -34,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,
|
||||
|
@ -34,7 +34,7 @@ use crate::{
|
||||
fs::{FileManager, FileSystem},
|
||||
settings::types::UnitLength,
|
||||
std::{args::Arg, StdLib},
|
||||
Program,
|
||||
ExecError, Program,
|
||||
};
|
||||
|
||||
/// State for executing a program.
|
||||
@ -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 {
|
||||
@ -1902,6 +1909,7 @@ impl ExecutorContext {
|
||||
program: &Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<Option<ModelingSessionData>, KclError> {
|
||||
let _stats = crate::log::LogPerfStats::new("Interpretation");
|
||||
// TODO: Use the top-level file's path.
|
||||
exec_state.add_module(std::path::PathBuf::from(""));
|
||||
// Before we even start executing the program, set the units.
|
||||
@ -2179,12 +2187,16 @@ impl ExecutorContext {
|
||||
&self,
|
||||
program: &Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<TakeSnapshot> {
|
||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||
self.execute_and_prepare(program, exec_state).await
|
||||
}
|
||||
|
||||
/// Execute the program, return the interpreter and outputs.
|
||||
pub async fn execute_and_prepare(&self, program: &Program, exec_state: &mut ExecState) -> Result<TakeSnapshot> {
|
||||
pub async fn execute_and_prepare(
|
||||
&self,
|
||||
program: &Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||
self.run(program, exec_state).await?;
|
||||
|
||||
// Zoom to fit.
|
||||
@ -2216,7 +2228,9 @@ impl ExecutorContext {
|
||||
modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
|
||||
} = resp
|
||||
else {
|
||||
anyhow::bail!("Unexpected response from engine: {:?}", resp);
|
||||
return Err(ExecError::BadPng(format!(
|
||||
"Instead of a TakeSnapshot response, the engine returned {resp:?}"
|
||||
)));
|
||||
};
|
||||
Ok(contents)
|
||||
}
|
||||
@ -3171,6 +3185,7 @@ let w = f() + f()
|
||||
inner: crate::ast::types::Program {
|
||||
body: Vec::new(),
|
||||
non_code_meta: Default::default(),
|
||||
shebang: None,
|
||||
digest: None,
|
||||
},
|
||||
start: 0,
|
||||
|
@ -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 {
|
||||
|
@ -8,11 +8,54 @@
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! println {
|
||||
($($rest:tt)*) => {
|
||||
#[cfg(feature = "disable-println")]
|
||||
{
|
||||
let _ = format!($($rest)*);
|
||||
}
|
||||
#[cfg(not(feature = "disable-println"))]
|
||||
std::println!($($rest)*)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! eprintln {
|
||||
($($rest:tt)*) => {
|
||||
#[cfg(feature = "disable-println")]
|
||||
{
|
||||
let _ = format!($($rest)*);
|
||||
}
|
||||
#[cfg(not(feature = "disable-println"))]
|
||||
std::eprintln!($($rest)*)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! print {
|
||||
($($rest:tt)*) => {
|
||||
#[cfg(feature = "disable-println")]
|
||||
{
|
||||
let _ = format!($($rest)*);
|
||||
}
|
||||
#[cfg(not(feature = "disable-println"))]
|
||||
std::print!($($rest)*)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! eprint {
|
||||
($($rest:tt)*) => {
|
||||
#[cfg(feature = "disable-println")]
|
||||
{
|
||||
let _ = format!($($rest)*);
|
||||
}
|
||||
#[cfg(not(feature = "disable-println"))]
|
||||
std::eprint!($($rest)*)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
#[global_allocator]
|
||||
static ALLOC: dhat::Alloc = dhat::Alloc;
|
||||
|
||||
mod ast;
|
||||
mod coredump;
|
||||
mod docs;
|
||||
@ -23,6 +66,7 @@ mod fs;
|
||||
mod function_param;
|
||||
mod kcl_value;
|
||||
pub mod lint;
|
||||
mod log;
|
||||
mod lsp;
|
||||
mod parser;
|
||||
mod settings;
|
||||
@ -42,13 +86,12 @@ pub use ast::modify::modify_ast_for_sketch;
|
||||
pub use ast::types::{FormatOptions, ModuleId};
|
||||
pub use coredump::CoreDump;
|
||||
pub use engine::{EngineManager, ExecutionKind};
|
||||
pub use errors::KclError;
|
||||
pub use errors::{ConnectionError, ExecError, KclError};
|
||||
pub use executor::{ExecState, ExecutorContext, ExecutorSettings, SourceRange};
|
||||
pub use lsp::copilot::Backend as CopilotLspBackend;
|
||||
pub use lsp::kcl::Backend as KclLspBackend;
|
||||
pub use lsp::kcl::Server as KclLspServerSubCommand;
|
||||
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
|
||||
pub use token::lexer;
|
||||
|
||||
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
|
||||
// Ideally we wouldn't export these things at all, they should only be used for testing.
|
||||
@ -72,6 +115,8 @@ pub mod std_utils {
|
||||
pub use crate::std::utils::{get_tangential_arc_to_info, is_points_ccw_wasm, TangentialArcInfoInput};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::log::{log, logln};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
116
src/wasm-lib/kcl/src/log.rs
Normal file
116
src/wasm-lib/kcl/src/log.rs
Normal file
@ -0,0 +1,116 @@
|
||||
#![allow(dead_code)]
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
use dhat::{HeapStats, Profiler};
|
||||
use std::env;
|
||||
use web_time::Instant;
|
||||
|
||||
const LOG_ENV_VAR: &str = "ZOO_LOG";
|
||||
lazy_static::lazy_static! {
|
||||
static ref ENABLED: bool = {
|
||||
let env_var = env::var(LOG_ENV_VAR);
|
||||
let Ok(env_var) = env_var else {
|
||||
return false;
|
||||
};
|
||||
!env_var.is_empty()
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref PROFILER: Profiler = Profiler::builder().testing().build();
|
||||
}
|
||||
|
||||
/// Log a message
|
||||
pub(crate) fn log(msg: impl Into<String>) {
|
||||
if *ENABLED {
|
||||
log_inner(msg.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! logln {
|
||||
($($rest:tt)*) => {
|
||||
crate::log::log(format!($($rest)*))
|
||||
}
|
||||
}
|
||||
pub(crate) use logln;
|
||||
|
||||
#[cfg(not(feature = "disable-println"))]
|
||||
#[inline]
|
||||
fn log_inner(msg: String) {
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "disable-println", target_arch = "wasm32"))]
|
||||
#[inline]
|
||||
fn log_inner(msg: String) {
|
||||
web_sys::console::log_1(&msg.into());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "disable-println", not(target_arch = "wasm32")))]
|
||||
#[inline]
|
||||
fn log_inner(_msg: String) {}
|
||||
|
||||
/// A helper struct for recording and logging basic performance metrics.
|
||||
///
|
||||
/// It will log the metrics when dropped or if `log_now` is called.
|
||||
pub(crate) struct LogPerfStats<'a> {
|
||||
msg: &'a str,
|
||||
start_time: Instant,
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
start_stats: HeapStats,
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
impl<'a> LogPerfStats<'a> {
|
||||
#[cfg(not(feature = "dhat-heap"))]
|
||||
pub fn new(msg: &'a str) -> Self {
|
||||
LogPerfStats {
|
||||
msg,
|
||||
start_time: Instant::now(),
|
||||
cancelled: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
pub fn new(msg: &'a str) -> Self {
|
||||
lazy_static::initialize(&PROFILER);
|
||||
LogPerfStats {
|
||||
msg,
|
||||
start_time: Instant::now(),
|
||||
start_stats: HeapStats::get(),
|
||||
cancelled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_now(&self) {
|
||||
let time = Instant::now().duration_since(self.start_time).as_secs_f64() * 1000.0;
|
||||
logln!("{}\n time: {time:.3}ms", self.msg);
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
{
|
||||
let stats = HeapStats::get();
|
||||
let blocks = stats.total_blocks - self.start_stats.total_blocks;
|
||||
let bytes = (stats.total_bytes - self.start_stats.total_bytes) as f64 / 1_000_000.0;
|
||||
let cur = stats.curr_bytes as f64 / 1000.0;
|
||||
let max = stats.curr_bytes as f64 / 1000.0;
|
||||
|
||||
logln!(" memory:");
|
||||
logln!(" allocations: {bytes:.5} MB ({blocks} blocks)");
|
||||
logln!(" currently allocated: {cur:.3} KB");
|
||||
logln!(" max allocated: {max:.3} KB");
|
||||
}
|
||||
}
|
||||
|
||||
/// After `cancel`ing, this object will not log its stats on drop (you can still can `log_now`).
|
||||
pub fn cancel(&mut self) {
|
||||
self.cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for LogPerfStats<'a> {
|
||||
fn drop(&mut self) {
|
||||
if !self.cancelled {
|
||||
self.log_now();
|
||||
}
|
||||
}
|
||||
}
|
@ -81,11 +81,16 @@ pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
|
||||
/// Invariants:
|
||||
/// - if there are no errors, then the Option will be Some
|
||||
/// - if the Option is None, then there will be at least one error in the ParseContext.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ParseResult(pub Result<(Option<Node<Program>>, ParseContext), KclError>);
|
||||
|
||||
impl ParseResult {
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub fn unwrap(self) -> Node<Program> {
|
||||
if self.0.is_err() || self.0.as_ref().unwrap().0.is_none() {
|
||||
eprint!("{self:#?}");
|
||||
}
|
||||
self.0.unwrap().0.unwrap()
|
||||
}
|
||||
|
||||
|
@ -15,15 +15,17 @@ 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::{
|
||||
math::BinaryExpressionToken, parser_impl::error::ContextError, PIPE_OPERATOR, PIPE_SUBSTITUTION_OPERATOR,
|
||||
},
|
||||
token::{Token, TokenType},
|
||||
unparser::ExprContext,
|
||||
};
|
||||
|
||||
pub(crate) mod error;
|
||||
@ -36,6 +38,7 @@ thread_local! {
|
||||
pub type TokenSlice<'slice, 'input> = &'slice mut &'input [Token];
|
||||
|
||||
pub fn run_parser(i: TokenSlice) -> super::ParseResult {
|
||||
let _stats = crate::log::LogPerfStats::new("Parsing");
|
||||
ParseContext::init();
|
||||
|
||||
let result = program.parse(i).save_err();
|
||||
@ -49,7 +52,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 +77,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 +144,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 +533,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 +556,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 +699,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 +710,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 +720,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.
|
||||
@ -917,26 +943,46 @@ fn if_expr(i: TokenSlice) -> PResult<BoxNode<IfExpression>> {
|
||||
))
|
||||
}
|
||||
|
||||
fn function_expr(i: TokenSlice) -> PResult<Expr> {
|
||||
let fn_tok = opt(fun).parse_next(i)?;
|
||||
ignore_whitespace(i);
|
||||
let (result, has_arrow) = function_decl.parse_next(i)?;
|
||||
if fn_tok.is_none() && !has_arrow {
|
||||
let err = KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: result.as_source_ranges(),
|
||||
message: "Anonymous function requires `fn` before `(`".to_owned(),
|
||||
});
|
||||
return Err(ErrMode::Cut(err.into()));
|
||||
}
|
||||
Ok(Expr::FunctionExpression(Box::new(result)))
|
||||
}
|
||||
|
||||
// Looks like
|
||||
// (arg0, arg1) => {
|
||||
// (arg0, arg1) {
|
||||
// const x = arg0 + arg1;
|
||||
// return x
|
||||
// }
|
||||
fn function_expression(i: TokenSlice) -> PResult<Node<FunctionExpression>> {
|
||||
fn function_decl(i: TokenSlice) -> PResult<(Node<FunctionExpression>, bool)> {
|
||||
fn return_type(i: TokenSlice) -> PResult<FnArgType> {
|
||||
colon(i)?;
|
||||
ignore_whitespace(i);
|
||||
argument_type(i)
|
||||
}
|
||||
|
||||
let open = open_paren(i)?;
|
||||
let start = open.start;
|
||||
let params = parameters(i)?;
|
||||
close_paren(i)?;
|
||||
ignore_whitespace(i);
|
||||
big_arrow(i)?;
|
||||
let arrow = opt(big_arrow).parse_next(i)?;
|
||||
ignore_whitespace(i);
|
||||
// Optional type arguments.
|
||||
let return_type = opt(argument_type).parse_next(i)?;
|
||||
// Optional return type.
|
||||
let return_type = opt(return_type).parse_next(i)?;
|
||||
ignore_whitespace(i);
|
||||
open_brace(i)?;
|
||||
let body = function_body(i)?;
|
||||
let end = close_brace(i)?.end;
|
||||
Ok(Node::new(
|
||||
let result = Node::new(
|
||||
FunctionExpression {
|
||||
params,
|
||||
body,
|
||||
@ -946,7 +992,21 @@ fn function_expression(i: TokenSlice) -> PResult<Node<FunctionExpression>> {
|
||||
start,
|
||||
end,
|
||||
open.module_id,
|
||||
))
|
||||
);
|
||||
|
||||
let has_arrow = if let Some(arrow) = arrow {
|
||||
ParseContext::warn(ParseError::with_suggestion(
|
||||
arrow.as_source_range(),
|
||||
Some(result.as_source_range()),
|
||||
"Unnecessary `=>` in function declaration",
|
||||
Some(""),
|
||||
));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok((result, has_arrow))
|
||||
}
|
||||
|
||||
/// E.g. `person.name`
|
||||
@ -1048,7 +1108,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 +1128,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 +1327,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Node<Program>> {
|
||||
Program {
|
||||
body,
|
||||
non_code_meta,
|
||||
shebang: None,
|
||||
digest: None,
|
||||
},
|
||||
start.0,
|
||||
@ -1456,7 +1515,7 @@ fn expr_allowed_in_pipe_expr(i: TokenSlice) -> PResult<Expr> {
|
||||
array,
|
||||
object.map(Box::new).map(Expr::ObjectExpression),
|
||||
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
|
||||
function_expression.map(Box::new).map(Expr::FunctionExpression),
|
||||
function_expr,
|
||||
if_expr.map(Expr::IfExpression),
|
||||
unnecessarily_bracketed,
|
||||
))
|
||||
@ -1521,31 +1580,40 @@ fn declaration(i: TokenSlice) -> PResult<BoxNode<VariableDeclaration>> {
|
||||
let (kind, mut start, dec_end, module_id) = if let Some((kind, token)) = &decl_token {
|
||||
(*kind, token.start, token.end, token.module_id)
|
||||
} else {
|
||||
// TODO warn on const
|
||||
(VariableKind::Const, id.start, id.end, id.module_id)
|
||||
};
|
||||
if let Some(token) = visibility_token {
|
||||
start = token.start;
|
||||
}
|
||||
|
||||
ignore_whitespace(i);
|
||||
equals(i)?;
|
||||
// After this point, the parser is DEFINITELY parsing a variable declaration, because
|
||||
// `fn`, `let`, `const` etc are all unambiguous. If you've parsed one of those tokens --
|
||||
// and we certainly have because `kind` was parsed above -- then the following tokens
|
||||
// MUST continue the variable declaration, otherwise the program is invalid.
|
||||
//
|
||||
// This means, from here until this function returns, any errors should be ErrMode::Cut,
|
||||
// not ErrMode::Backtrack. Because the parser is definitely parsing a variable declaration.
|
||||
// If there's an error, there's no point backtracking -- instead the parser should fail.
|
||||
ignore_whitespace(i);
|
||||
|
||||
let val = if kind == VariableKind::Fn {
|
||||
function_expression
|
||||
.map(Box::new)
|
||||
let eq = opt(equals).parse_next(i)?;
|
||||
ignore_whitespace(i);
|
||||
|
||||
let val = function_decl
|
||||
.map(|t| Box::new(t.0))
|
||||
.map(Expr::FunctionExpression)
|
||||
.context(expected("a KCL function expression, like () => { return 1 }"))
|
||||
.parse_next(i)
|
||||
.context(expected("a KCL function expression, like () { return 1 }"))
|
||||
.parse_next(i);
|
||||
|
||||
if let Some(t) = eq {
|
||||
let ctxt_end = val.as_ref().map(|e| e.end()).unwrap_or(t.end);
|
||||
ParseContext::warn(ParseError::with_suggestion(
|
||||
t.as_source_range(),
|
||||
Some(SourceRange([id.start, ctxt_end, module_id.as_usize()])),
|
||||
"Unnecessary `=` in function declaration",
|
||||
Some(""),
|
||||
));
|
||||
}
|
||||
|
||||
val
|
||||
} else {
|
||||
equals(i)?;
|
||||
ignore_whitespace(i);
|
||||
|
||||
expression
|
||||
.try_map(|val| {
|
||||
// Function bodies can be used if and only if declaring a function.
|
||||
@ -1919,16 +1987,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<()> {
|
||||
@ -1936,6 +2002,17 @@ fn question_mark(i: TokenSlice) -> PResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fun(i: TokenSlice) -> PResult<Token> {
|
||||
any.try_map(|token: Token| match token.token_type {
|
||||
TokenType::Keyword if token.value == "fn" => Ok(token),
|
||||
_ => Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: token.as_source_ranges(),
|
||||
message: format!("expected 'fn', found {}", token.value.as_str(),),
|
||||
})),
|
||||
})
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
/// Parse a comma, optionally followed by some whitespace.
|
||||
fn comma_sep(i: TokenSlice) -> PResult<()> {
|
||||
(opt(whitespace), comma, opt(whitespace))
|
||||
@ -1959,6 +2036,7 @@ fn arguments(i: TokenSlice) -> PResult<Vec<Expr>> {
|
||||
fn argument_type(i: TokenSlice) -> PResult<FnArgType> {
|
||||
let type_ = alt((
|
||||
// Object types
|
||||
// TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`.
|
||||
(open_brace, parameters, close_brace).map(|(_, params, _)| Ok(FnArgType::Object { properties: params })),
|
||||
// Array types
|
||||
(one_of(TokenType::Type), open_bracket, close_bracket).map(|(token, _, _)| {
|
||||
@ -1989,13 +2067,11 @@ fn argument_type(i: TokenSlice) -> PResult<FnArgType> {
|
||||
}
|
||||
|
||||
fn parameter(i: TokenSlice) -> PResult<(Token, std::option::Option<FnArgType>, bool)> {
|
||||
let (arg_name, optional, _, _, _, type_) = (
|
||||
let (arg_name, optional, _, type_) = (
|
||||
any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"),
|
||||
opt(question_mark),
|
||||
opt(whitespace),
|
||||
opt(colon),
|
||||
opt(whitespace),
|
||||
opt(argument_type),
|
||||
opt((colon, opt(whitespace), argument_type).map(|tup| tup.2)),
|
||||
)
|
||||
.parse_next(i)?;
|
||||
Ok((arg_name, type_, optional.is_some()))
|
||||
@ -2071,57 +2147,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, ExprContext::Other);
|
||||
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,
|
||||
@ -2129,7 +2232,6 @@ fn fn_call(i: TokenSlice) -> PResult<Node<CallExpression>> {
|
||||
inner: CallExpression {
|
||||
callee: fn_name,
|
||||
arguments: args,
|
||||
optional: false,
|
||||
digest: None,
|
||||
},
|
||||
})
|
||||
@ -2184,7 +2286,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn weird_program_unclosed_paren() {
|
||||
let tokens = crate::token::lexer("fn firstPrime=(", ModuleId::default()).unwrap();
|
||||
let tokens = crate::token::lexer("fn firstPrime(", ModuleId::default()).unwrap();
|
||||
let last = tokens.last().unwrap();
|
||||
let err: super::error::ErrorKind = program.parse(&tokens).unwrap_err().into();
|
||||
let err = err.unwrap_parse_error();
|
||||
@ -2236,7 +2338,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_comments_in_function1() {
|
||||
let test_program = r#"() => {
|
||||
let test_program = r#"() {
|
||||
// comment 0
|
||||
const a = 1
|
||||
// comment 1
|
||||
@ -2246,7 +2348,7 @@ mod tests {
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(test_program, ModuleId::default()).unwrap();
|
||||
let mut slice = tokens.as_slice();
|
||||
let expr = function_expression.parse_next(&mut slice).unwrap();
|
||||
let expr = function_decl.map(|t| t.0).parse_next(&mut slice).unwrap();
|
||||
assert_eq!(expr.params, vec![]);
|
||||
let comment_start = expr.body.non_code_meta.start_nodes.first().unwrap();
|
||||
let comment0 = &expr.body.non_code_meta.non_code_nodes.get(&0).unwrap()[0];
|
||||
@ -2258,13 +2360,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_comments_in_function2() {
|
||||
let test_program = r#"() => {
|
||||
const yo = { a: { b: { c: '123' } } } /* block
|
||||
let test_program = r#"() {
|
||||
const yo = { a = { b = { c = '123' } } } /* block
|
||||
comment */
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(test_program, ModuleId::default()).unwrap();
|
||||
let mut slice = tokens.as_slice();
|
||||
let expr = function_expression.parse_next(&mut slice).unwrap();
|
||||
let expr = function_decl.map(|t| t.0).parse_next(&mut slice).unwrap();
|
||||
let comment0 = &expr.body.non_code_meta.non_code_nodes.get(&0).unwrap()[0];
|
||||
assert_eq!(comment0.value(), "block\ncomment");
|
||||
}
|
||||
@ -2316,25 +2418,25 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
|
||||
#[test]
|
||||
fn test_whitespace_in_function() {
|
||||
let test_program = r#"() => {
|
||||
let test_program = r#"() {
|
||||
return sg
|
||||
return sg
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(test_program, ModuleId::default()).unwrap();
|
||||
let mut slice = tokens.as_slice();
|
||||
let _expr = function_expression.parse_next(&mut slice).unwrap();
|
||||
let _expr = function_decl.parse_next(&mut slice).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_lines_in_function() {
|
||||
let test_program = "() => {
|
||||
let test_program = "() {
|
||||
|
||||
return 2
|
||||
}";
|
||||
let module_id = ModuleId::from_usize(1);
|
||||
let tokens = crate::token::lexer(test_program, module_id).unwrap();
|
||||
let mut slice = tokens.as_slice();
|
||||
let expr = function_expression.parse_next(&mut slice).unwrap();
|
||||
let expr = function_decl.map(|t| t.0).parse_next(&mut slice).unwrap();
|
||||
assert_eq!(
|
||||
expr,
|
||||
Node::new(
|
||||
@ -2350,14 +2452,14 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
raw: "2".to_owned(),
|
||||
digest: None,
|
||||
},
|
||||
32,
|
||||
33,
|
||||
29,
|
||||
30,
|
||||
module_id,
|
||||
))),
|
||||
digest: None,
|
||||
},
|
||||
25,
|
||||
33,
|
||||
22,
|
||||
30,
|
||||
module_id,
|
||||
))],
|
||||
non_code_meta: NonCodeMeta {
|
||||
@ -2367,23 +2469,24 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
value: NonCodeValue::NewLine,
|
||||
digest: None
|
||||
},
|
||||
7,
|
||||
25,
|
||||
4,
|
||||
22,
|
||||
module_id,
|
||||
)],
|
||||
digest: None,
|
||||
},
|
||||
shebang: None,
|
||||
digest: None,
|
||||
},
|
||||
7,
|
||||
47,
|
||||
4,
|
||||
44,
|
||||
module_id,
|
||||
),
|
||||
return_type: None,
|
||||
digest: None,
|
||||
},
|
||||
0,
|
||||
47,
|
||||
44,
|
||||
module_id,
|
||||
)
|
||||
);
|
||||
@ -2418,7 +2521,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 +2558,8 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
},
|
||||
digest: None,
|
||||
},
|
||||
60,
|
||||
82,
|
||||
63,
|
||||
85,
|
||||
module_id,
|
||||
),
|
||||
Node::new(
|
||||
@ -2464,8 +2567,8 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
value: NonCodeValue::NewLine,
|
||||
digest: None,
|
||||
},
|
||||
82,
|
||||
86,
|
||||
85,
|
||||
89,
|
||||
module_id,
|
||||
)
|
||||
]),
|
||||
@ -2481,8 +2584,8 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
},
|
||||
digest: None,
|
||||
},
|
||||
103,
|
||||
129,
|
||||
106,
|
||||
132,
|
||||
module_id,
|
||||
)]),
|
||||
non_code_meta.non_code_nodes.get(&1),
|
||||
@ -2819,7 +2922,7 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
let test_fn = "(let) => { return 1 }";
|
||||
let module_id = ModuleId::from_usize(2);
|
||||
let tokens = crate::token::lexer(test_fn, module_id).unwrap();
|
||||
let err = function_expression.parse(&tokens).unwrap_err().into_inner();
|
||||
let err = function_decl.parse(&tokens).unwrap_err().into_inner();
|
||||
let cause = err.cause.unwrap();
|
||||
// This is the token `let`
|
||||
assert_eq!(cause.source_ranges(), vec![SourceRange([1, 4, 2])]);
|
||||
@ -2858,7 +2961,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]
|
||||
@ -2892,12 +2995,12 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
|
||||
#[test]
|
||||
fn test_user_function() {
|
||||
let input = "() => {
|
||||
let input = "() {
|
||||
return 2
|
||||
}";
|
||||
|
||||
let tokens = crate::token::lexer(input, ModuleId::default()).unwrap();
|
||||
let actual = function_expression.parse(&tokens);
|
||||
let actual = function_decl.parse(&tokens);
|
||||
assert!(actual.is_ok(), "could not parse test function");
|
||||
}
|
||||
|
||||
@ -3110,6 +3213,7 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
4,
|
||||
module_id,
|
||||
))],
|
||||
shebang: None,
|
||||
non_code_meta: NonCodeMeta::default(),
|
||||
digest: None,
|
||||
},
|
||||
@ -3128,6 +3232,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(), "found: {:#?}", result.1.errors);
|
||||
(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);
|
||||
@ -3209,6 +3321,16 @@ const height = [obj["a"] -1, 0]"#;
|
||||
crate::parser::top_level_parse(code).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anon_fn() {
|
||||
crate::parser::top_level_parse("foo(42, fn(x) { return x + 1 })").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anon_fn_no_fn() {
|
||||
assert_err_contains("foo(42, (x) { return x + 1 })", "Anonymous function requires `fn`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_half_pipe() {
|
||||
let code = "const height = 10
|
||||
@ -3528,7 +3650,7 @@ e
|
||||
|
||||
#[test]
|
||||
fn test_keyword_ok_in_fn_args_return() {
|
||||
let some_program_string = r#"fn thing = (param) => {
|
||||
let some_program_string = r#"fn thing(param) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -3658,6 +3780,47 @@ 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)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warn_fn_decl() {
|
||||
let some_program_string = r#"fn foo = () => {
|
||||
return 0
|
||||
}"#;
|
||||
let (_, ctxt) = assert_no_err(some_program_string);
|
||||
assert_eq!(ctxt.warnings.len(), 2);
|
||||
let replaced = ctxt.warnings[0].apply_suggestion(some_program_string).unwrap();
|
||||
let replaced = ctxt.warnings[1].apply_suggestion(&replaced).unwrap();
|
||||
// Note the whitespace here is bad, but we're just testing the suggestion spans really. In
|
||||
// real life we might reformat after applying suggestions.
|
||||
assert_eq!(
|
||||
replaced,
|
||||
r#"fn foo () {
|
||||
return 0
|
||||
}"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -3673,11 +3836,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 +3875,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 +3885,7 @@ mod snapshot_tests {
|
||||
settings.bind(|| {
|
||||
insta::assert_json_snapshot!(actual);
|
||||
});
|
||||
let _ = ParseContext::take();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3649
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -52,7 +51,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 39,
|
||||
"optional": false,
|
||||
"start": 18,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -97,7 +95,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 63,
|
||||
"optional": false,
|
||||
"start": 47,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -149,7 +146,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 96,
|
||||
"optional": false,
|
||||
"start": 71,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -201,7 +197,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 121,
|
||||
"optional": false,
|
||||
"start": 104,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -230,7 +225,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 143,
|
||||
"optional": false,
|
||||
"start": 129,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3720
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -68,7 +67,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 23,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3722
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -64,7 +63,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 80,
|
||||
"optional": false,
|
||||
"start": 62,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -83,7 +83,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 66,
|
||||
"optional": false,
|
||||
"start": 54,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3737
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -52,7 +51,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 37,
|
||||
"optional": false,
|
||||
"start": 17,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -104,7 +102,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 75,
|
||||
"optional": false,
|
||||
"start": 49,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -149,7 +146,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 104,
|
||||
"optional": false,
|
||||
"start": 87,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -201,7 +197,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 145,
|
||||
"optional": false,
|
||||
"start": 116,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -222,7 +217,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 165,
|
||||
"optional": false,
|
||||
"start": 157,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3745
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -52,7 +51,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 37,
|
||||
"optional": false,
|
||||
"start": 17,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -97,7 +95,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 58,
|
||||
"optional": false,
|
||||
"start": 41,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -118,7 +115,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 70,
|
||||
"optional": false,
|
||||
"start": 62,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -32,7 +32,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 30,
|
||||
"optional": false,
|
||||
"start": 14,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3750
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -36,7 +35,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 18,
|
||||
"optional": false,
|
||||
"start": 14,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -65,7 +63,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 29,
|
||||
"optional": false,
|
||||
"start": 22,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3751
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -35,7 +34,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 30,
|
||||
"optional": false,
|
||||
"start": 14,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -79,7 +77,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 49,
|
||||
"optional": false,
|
||||
"start": 34,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3752
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -61,7 +60,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 22,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3753
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -96,7 +95,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 36,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3754
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -61,7 +60,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 19,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3755
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -96,7 +95,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 35,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3756
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -96,7 +95,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 35,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3757
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -50,7 +49,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 37,
|
||||
"optional": false,
|
||||
"start": 17,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3758
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -41,7 +40,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 28,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3760
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -47,7 +46,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 15,
|
||||
"optional": false,
|
||||
"start": 0,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3761
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -36,7 +35,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 36,
|
||||
"optional": false,
|
||||
"start": 17,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -119,7 +117,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 85,
|
||||
"optional": false,
|
||||
"start": 44,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -148,7 +145,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 107,
|
||||
"optional": false,
|
||||
"start": 93,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3762
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -45,7 +44,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 47,
|
||||
"optional": false,
|
||||
"start": 28,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3658
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -53,7 +52,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 35,
|
||||
"optional": false,
|
||||
"start": 23,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -72,7 +70,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 36,
|
||||
"optional": false,
|
||||
"start": 14,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -35,7 +35,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 38,
|
||||
"optional": false,
|
||||
"start": 19,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -56,7 +55,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 132,
|
||||
"optional": false,
|
||||
"start": 115,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3822
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -45,7 +44,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 64,
|
||||
"optional": false,
|
||||
"start": 52,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3660
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -45,7 +44,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 31,
|
||||
"optional": false,
|
||||
"start": 19,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -72,7 +70,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 35,
|
||||
"optional": false,
|
||||
"start": 14,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3661
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -65,7 +64,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 36,
|
||||
"optional": false,
|
||||
"start": 23,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,7 +1,6 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
{
|
||||
"body": [
|
||||
|
@ -32,7 +32,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 29,
|
||||
"optional": false,
|
||||
"start": 11,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3718
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
@ -35,7 +34,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 29,
|
||||
"optional": false,
|
||||
"start": 11,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
@ -86,7 +84,6 @@ snapshot_kind: text
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 53,
|
||||
"optional": false,
|
||||
"start": 33,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
|
@ -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}")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -31,7 +31,7 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
/// `[f(a), f(b), f(c)]`
|
||||
/// ```no_run
|
||||
/// const r = 10 // radius
|
||||
/// fn drawCircle = (id) => {
|
||||
/// fn drawCircle(id) {
|
||||
/// return startSketchOn("XY")
|
||||
/// |> circle({ center: [id * 2 * r, 0], radius: r}, %)
|
||||
/// }
|
||||
@ -49,7 +49,7 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
/// // Call `map`, using an anonymous function instead of a named one.
|
||||
/// const circles = map(
|
||||
/// [1..3],
|
||||
/// (id) => {
|
||||
/// fn(id) {
|
||||
/// return startSketchOn("XY")
|
||||
/// |> circle({ center: [id * 2 * r, 0], radius: r}, %)
|
||||
/// }
|
||||
@ -149,7 +149,7 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
||||
/// // Use a `reduce` to draw the remaining decagon sides.
|
||||
/// // For each number in the array 1..10, run the given function,
|
||||
/// // which takes a partially-sketched decagon and adds one more edge to it.
|
||||
/// fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) => {
|
||||
/// fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
|
||||
/// // Draw one edge of the decagon.
|
||||
/// let x = cos(stepAngle * i) * radius
|
||||
/// let y = sin(stepAngle * i) * radius
|
||||
|
@ -27,7 +27,7 @@ pub async fn int(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
||||
/// startSketchOn('XZ')
|
||||
/// |> circle({ center = [0, 0], radius = 2 }, %)
|
||||
/// |> extrude(5, %)
|
||||
/// |> patternTransform(n, (id) => {
|
||||
/// |> patternTransform(n, fn(id) {
|
||||
/// return { translate = [4 * id, 0, 0] }
|
||||
/// }, %)
|
||||
/// ```
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user