Compare commits

...

12 Commits

Author SHA1 Message Date
dac3c0c224 Delete test that is covering a basic assumption 2025-05-13 12:46:28 -04:00
99cb6a6179 [Fix] Created the safest navigate/load/open/please sir i want some more for a .kcl file (#6867)
* fix: clear scene and bust cache if rust panics

* Update onboarding following @jgomez720

* chore: hopefully made a safe navigate to kcl file to call executeAST without a race condition

* chore: hopefully made a safe navigate to kcl file to call executeAST without a race condition

* fix: clean up

* fix: FUCK

* fix: FUCK 2.0

* fix: oh boi

* fix: oh boi

* fix: idk man

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix: take main on this, do not need a single line from my testing code

* fix: more PR cleanup from all of the testing code

* fix: trying to clean up more, ope this has a lot of other code

* fix: PR clean up

* fix: trying to get a clean branch, I had multiple other branches in here ope

* fix: more cleanup

* fix: another one

* fix: fixed the comment to be accurate

* fix: removed confusing comment

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2025-05-13 15:35:29 +00:00
13dbfdfaa4 SystemIOMachine: Toast default error instead of empty string (#6898)
pierremtb/issue6820-add-error-instead-of-empty-string
2025-05-13 10:28:24 -05:00
8603f5c53a Default to the feature tree, code, and file panes open (#6901)
Only include the file pane on desktop. This should encourage users to
try point-and-click and feature tree workflows. After #6629 is
completed, we should default to the code pane being closed.
2025-05-13 15:25:27 +00:00
d5cc9e8386 Check both public and internal KCL samples (#6889) 2025-05-13 09:21:53 -04:00
533e17466b Update text-to-cad success toast to tell what file got made (#6882)
Closes #5820 and provides a bit of other polish to make this toast look
a bit better.
2025-05-13 12:26:29 +00:00
01c7b69f50 Function types (#6891)
* Change Fn to fn for function types

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

* Support args and return types in function types

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

* Use fancy function types in the docs

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-05-13 14:59:23 +12:00
47feae3bd9 Register modeling commands as disabled, enable without needing to re-open the command palette (#6886)
* Allow adding and removing commands from any command bar state

* Allow commands to be configured disabled in the combobox

* Set up modeling commands to toggle `disabled` based on network status, instead of filtering

* Fix tsc
2025-05-13 01:18:50 +00:00
e7ecd655c4 Add docs on arrays and ranges (#6890)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-05-13 13:06:07 +12:00
d9e538c6ea Fix docs for object properties (#6892)
Jon removed support for obj["a"] a while ago in #6529 aka
2e754f2a11
2025-05-13 00:56:50 +00:00
e297f8286f KCL: Fix incorrect error messages (#6883)
**Problem:**

KCL's xLine and yLine functions were telling users to supply either "end" or "endAbsolute" arguments. But "end" is not a valid argument for xLine/yLine, it's actually "length" instead.

**Cause:**

xLine/yLine were using a helper function designed for `line` KCL stdlib functions, which do use `end`. 

**Solution**

Add a new param to the helper function, indicating what the label should be for relative lines. "end" for `line` calls, `length` for x/yline
2025-05-13 00:26:43 +00:00
b3bc90bbe4 Fix lower right version background in dark mode and update snapshots (#6888)
* pierremtb/adhoc/fix-version-bg-dark-mode

* Update snapshots
2025-05-12 23:46:52 +00:00
61 changed files with 470 additions and 171 deletions

View File

@ -213,7 +213,13 @@ jobs:
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: npm run playwright install chromium --with-deps
- name: run unit tests for kcl samples
- name: Download internal KCL samples
run: git clone --depth=1 https://x-access-token:${{ secrets.GH_PAT_KCL_SAMPLES_INTERNAL }}@github.com/KittyCAD/kcl-samples-internal public/kcl-samples/internal
- name: Regenerate KCL samples manifest
run: cd rust/kcl-lib && EXPECTORATE=overwrite cargo test generate_manifest
- name: Check public and internal KCL samples
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: npm run test:unit:kcl-samples
env:

32
docs/kcl-lang/arrays.md Normal file
View File

@ -0,0 +1,32 @@
---
title: "Arrays and ranges"
excerpt: "Documentation of the KCL language for the Zoo Design Studio."
layout: manual
---
Arrays are sequences of values.
Arrays can be written out as *array literals* using a sequence of expressions surrounded by square brackets, e.g., `['hello', 'world']` is an array of strings, `[x, x + 1, x + 2]` is an array of numbers (assuming `x` is a number), `[]` is an empty array, and `['hello', 42, true]` is a mixed array.
A value in an array can be accessed by indexing using square brackets where the index is a number, for example, `arr[0]`, `arr[42]`, `arr[i]` (where `arr` is an array and `i` is a (whole) number).
There are some useful functions for working with arrays in the standard library, see [std::array](/docs/kcl-std/modules/std-array) for details.
## Array types
Arrays have their own types: `[T]` where `T` is the type of the elements of the array, for example, `[string]` means an array of `string`s and `[any]` means an array of any values.
Array types can also include length information: `[T; n]` denotes an array of length `n` (where `n` is a number literal) and `[T; 1+]` denotes an array whose length is at least one (i.e., a non-empty array). E.g., `[string; 1+]` and `[number(mm); 3]` are valid array types.
## Ranges
Ranges are a succinct way to create an array of sequential numbers. The syntax is `[start .. end]` where `start` and `end` evaluate to whole numbers (integers). Ranges are inclusive of the start and end. The end must be greater than the start. Examples:
```kcl,norun
[0..3] // [0, 1, 2, 3]
[3..10] // [3, 4, 5, 6, 7, 8, 9, 10]
x = 2
[x..x+1] // [2, 3]
```
The units of the start and end numbers must be the same and the result inherits those units.

View File

@ -14,6 +14,7 @@ things in a more tutorial fashion. See also our documentation of the [standard l
* [Values and types](/docs/kcl-lang/types)
* [Numeric types and units](/docs/kcl-lang/numeric)
* [Functions](/docs/kcl-lang/functions)
* [Arrays and ranges](/docs/kcl-lang/arrays)
* [Projects and modules](/docs/kcl-lang/modules)
* [Attributes](/docs/kcl-lang/attributes)
* [Importing geometry from other CAD systems](/docs/kcl-lang/foreign-imports)

View File

@ -19,18 +19,6 @@ myBool = false
Currently you cannot redeclare a constant.
## Arrays
An array is defined with `[]` braces. What is inside the brackets can
be of any type. For example, the following is completely valid:
```
myArray = ["thing", 2, false]
```
If you want to get a value from an array you can use the index like so:
`myArray[0]`.
## Objects
@ -40,8 +28,8 @@ An object is defined with `{}` braces. Here is an example object:
myObj = { a = 0, b = "thing" }
```
We support two different ways of getting properties from objects, you can call
`myObj.a` or `myObj["a"]` both work.
To get the property of an object, you can call `myObj.a`, which in the above
example returns 0.
## `ImportedGeometry`

View File

@ -10,7 +10,7 @@ Apply a function to every element of a list.
```kcl
map(
@array: [any],
f: Fn,
f: fn(any): any,
): [any]
```
@ -22,7 +22,7 @@ Given a list like `[a, b, c]`, and a function like `f`, returns
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `array` | [`[any]`](/docs/kcl-std/types/std-types-any) | Input array. The output array is this input array, but every element has had the function `f` run on it. | Yes |
| `f` | [`Fn`](/docs/kcl-std/types/std-types-Fn) | A function. The output array is just the input array, but `f` has been run on every item. | Yes |
| `f` | [`fn(any): any`](/docs/kcl-std/types/std-types-fn) | A function. The output array is just the input array, but `f` has been run on every item. | Yes |
### Returns

View File

@ -11,7 +11,7 @@ layout: manual
reduce(
@array: [any],
initial: any,
f: Fn,
f: fn(any, accum: any): any,
): [any]
```
@ -24,7 +24,7 @@ using the previous value and the element.
|----------|------|-------------|----------|
| `array` | [`[any]`](/docs/kcl-std/types/std-types-any) | Each element of this array gets run through the function `f`, combined with the previous output from `f`, and then used for the next run. | Yes |
| `initial` | [`any`](/docs/kcl-std/types/std-types-any) | The first time `f` is run, it will be called with the first item of `array` and this initial starting value. | Yes |
| `f` | [`Fn`](/docs/kcl-std/types/std-types-Fn) | Run once per item in the input `array`. This function takes an item from the array, and the previous output from `f` (or `initial` on the very first run). The final time `f` is run, its output is returned as the final output from `reduce`. | Yes |
| `f` | [`fn(any, accum: any): any`](/docs/kcl-std/types/std-types-fn) | Run once per item in the input `array`. This function takes an item from the array, and the previous output from `f` (or `initial` on the very first run). The final time `f` is run, its output is returned as the final output from `reduce`. | Yes |
### Returns

View File

@ -140,13 +140,13 @@ See also the [types overview](/docs/kcl-lang/types)
* [**Primitive types**](/docs/kcl-lang/types)
* [`End`](/docs/kcl-lang/types#End)
* [`Fn`](/docs/kcl-std/types/std-types-Fn)
* [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry)
* [`Start`](/docs/kcl-lang/types#Start)
* [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator)
* [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier)
* [`any`](/docs/kcl-std/types/std-types-any)
* [`bool`](/docs/kcl-std/types/std-types-bool)
* [`fn`](/docs/kcl-std/types/std-types-fn)
* [`number`](/docs/kcl-std/types/std-types-number)
* [`string`](/docs/kcl-std/types/std-types-string)
* [`tag`](/docs/kcl-std/types/std-types-tag)

View File

@ -17,7 +17,6 @@ Types can (optionally) be used to describe a function's arguments and returned v
* [`Axis3d`](/docs/kcl-std/types/std-types-Axis3d)
* [`Edge`](/docs/kcl-std/types/std-types-Edge)
* [`Face`](/docs/kcl-std/types/std-types-Face)
* [`Fn`](/docs/kcl-std/types/std-types-Fn)
* [`Helix`](/docs/kcl-std/types/std-types-Helix)
* [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry)
* [`Plane`](/docs/kcl-std/types/std-types-Plane)
@ -27,6 +26,7 @@ Types can (optionally) be used to describe a function's arguments and returned v
* [`Solid`](/docs/kcl-std/types/std-types-Solid)
* [`any`](/docs/kcl-std/types/std-types-any)
* [`bool`](/docs/kcl-std/types/std-types-bool)
* [`fn`](/docs/kcl-std/types/std-types-fn)
* [`number`](/docs/kcl-std/types/std-types-number)
* [`string`](/docs/kcl-std/types/std-types-string)
* [`tag`](/docs/kcl-std/types/std-types-tag)

View File

@ -1,5 +1,5 @@
---
title: "Fn"
title: "fn"
subtitle: "Type in std::types"
excerpt: "The type of any function in KCL."
layout: manual

View File

@ -549,48 +549,6 @@ extrude002 = extrude(profile002, length = 150)
})
})
test(
`Network health indicator only appears in modeling view`,
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'),
path.join(bracketDir, 'main.kcl')
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page)
// Locators
const projectsHeading = page.getByRole('heading', {
name: 'Projects',
})
const projectLink = page.getByRole('link', { name: 'bracket' })
const networkHealthIndicator = page.getByTestId('network-toggle')
await test.step('Check the home page', async () => {
await expect(projectsHeading).toBeVisible()
await expect(projectLink).toBeVisible()
await expect(networkHealthIndicator).not.toBeVisible()
})
await test.step('Open the project', async () => {
await projectLink.click()
})
await test.step('Check the modeling view', async () => {
await expect(networkHealthIndicator).toBeVisible()
await expect(networkHealthIndicator).toContainText('Problem')
await u.waitForPageLoad()
await expect(networkHealthIndicator).toContainText('Connected')
})
}
)
test(`View gizmo stays visible even when zoomed out all the way`, async ({
page,
homePage,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -905,7 +905,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('Enter')
// Go into the project that was created from Text to CAD
await page.getByText(projectName).click()
await homePage.openProject(projectName)
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
await expect(page.getByTestId('app-header-project-name')).toContainText(
@ -951,7 +951,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('Enter')
// Go into the project that was created from Text to CAD
await page.getByText(projectName).click()
await homePage.openProject(projectName)
await page.getByRole('button', { name: 'Accept' }).click()

View File

@ -674,6 +674,8 @@ fn cleanup_type_string(input: &str, fmt_for_text: bool) -> String {
if fmt_for_text && ty.starts_with("number") {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-number)")
} else if fmt_for_text && ty.starts_with("fn") {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-fn)")
} else if fmt_for_text && SPECIAL_TYPES.contains(&ty) {
format!("[{prefix}{ty}{suffix}](/docs/kcl-lang/types#{ty})")
} else if fmt_for_text && DECLARED_TYPES.contains(&ty) {

View File

@ -39,7 +39,7 @@ const DECLARED_TYPES: [&str; 17] = [
"Axis2d",
"Axis3d",
"ImportedGeometry",
"Fn",
"fn",
];
lazy_static::lazy_static! {

View File

@ -183,7 +183,7 @@ impl RuntimeType {
AstPrimitiveType::Named(name) => Self::from_alias(&name.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
AstPrimitiveType::Function => RuntimeType::Primitive(PrimitiveType::Function),
AstPrimitiveType::Function(_) => RuntimeType::Primitive(PrimitiveType::Function),
})
}

View File

@ -2,8 +2,8 @@ use sha2::{Digest as DigestTrait, Sha256};
use crate::parsing::ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryPart, BodyItem,
CallExpressionKw, DefaultParamVal, ElseIf, Expr, ExpressionStatement, FunctionExpression, Identifier, IfExpression,
ImportItem, ImportSelector, ImportStatement, ItemVisibility, KclNone, LabelledExpression, Literal,
CallExpressionKw, DefaultParamVal, ElseIf, Expr, ExpressionStatement, FunctionExpression, FunctionType, Identifier,
IfExpression, ImportItem, ImportSelector, ImportStatement, ItemVisibility, KclNone, LabelledExpression, Literal,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name, ObjectExpression, ObjectProperty, Parameter,
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, TagDeclarator, Type, TypeDeclaration,
UnaryExpression, VariableDeclaration, VariableDeclarator, VariableKind,
@ -233,13 +233,28 @@ impl PrimitiveType {
PrimitiveType::Boolean => hasher.update(b"bool"),
PrimitiveType::Tag => hasher.update(b"tag"),
PrimitiveType::ImportedGeometry => hasher.update(b"ImportedGeometry"),
PrimitiveType::Function => hasher.update(b"Fn"),
PrimitiveType::Function(f) => hasher.update(f.compute_digest()),
}
hasher.finalize().into()
}
}
impl FunctionType {
compute_digest!(|slf, hasher| {
if let Some(u) = &mut slf.unnamed_arg {
hasher.update(u.compute_digest());
}
slf.named_args.iter_mut().for_each(|(a, t)| {
a.compute_digest();
t.compute_digest();
});
if let Some(r) = &mut slf.return_type {
hasher.update(r.compute_digest());
}
});
}
impl Parameter {
compute_digest!(|slf, hasher| {
hasher.update(slf.identifier.compute_digest());

View File

@ -3199,8 +3199,8 @@ pub enum PrimitiveType {
Tag,
/// Imported from other CAD system.
ImportedGeometry,
/// `Fn`, type of functions.
Function,
/// `fn`, type of functions.
Function(FunctionType),
/// An identifier used as a type (not really a primitive type, but whatever).
Named(Node<Identifier>),
}
@ -3215,7 +3215,6 @@ impl PrimitiveType {
("number", None) => Some(PrimitiveType::Number(NumericSuffix::None)),
("number", Some(s)) => Some(PrimitiveType::Number(s)),
("ImportedGeometry", None) => Some(PrimitiveType::ImportedGeometry),
("Fn", None) => Some(PrimitiveType::Function),
_ => None,
}
}
@ -3236,12 +3235,57 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Boolean => write!(f, "bool"),
PrimitiveType::Tag => write!(f, "tag"),
PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"),
PrimitiveType::Function => write!(f, "Fn"),
PrimitiveType::Function(t) => {
write!(f, "fn")?;
if t.unnamed_arg.is_some() || !t.named_args.is_empty() || t.return_type.is_some() {
write!(f, "(")?;
if let Some(u) = &t.unnamed_arg {
write!(f, "{u}")?;
if !t.named_args.is_empty() {
write!(f, ", ")?;
}
}
for (i, (a, t)) in t.named_args.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "{}: {t}", a.name)?;
}
write!(f, ")")?;
if let Some(r) = &t.return_type {
write!(f, ": {r}")?;
}
}
Ok(())
}
PrimitiveType::Named(n) => write!(f, "{}", n.name),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct FunctionType {
pub unnamed_arg: Option<BoxNode<Type>>,
pub named_args: Vec<(Node<Identifier>, Node<Type>)>,
pub return_type: Option<BoxNode<Type>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
impl FunctionType {
pub fn empty_fn_type() -> Self {
FunctionType {
unnamed_arg: None,
named_args: Vec::new(),
return_type: None,
digest: None,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -3293,7 +3337,7 @@ impl fmt::Display for Type {
} else {
write!(f, ",")?;
}
write!(f, "{}: ", p.identifier.name)?;
write!(f, " {}:", p.identifier.name)?;
if let Some(ty) = &p.type_ {
write!(f, " {}", ty.inner)?;
}

View File

@ -25,11 +25,11 @@ use crate::{
ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
BoxNode, CallExpressionKw, CommentStyle, DefaultParamVal, ElseIf, Expr, ExpressionStatement,
FunctionExpression, Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement, ItemVisibility,
LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name, Node, NodeList,
NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression,
PipeSubstitution, PrimitiveType, Program, ReturnStatement, Shebang, TagDeclarator, Type, TypeDeclaration,
UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
FunctionExpression, FunctionType, Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement,
ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name,
Node, NodeList, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter,
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, Shebang, TagDeclarator, Type,
TypeDeclaration, UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
},
math::BinaryExpressionToken,
token::{Token, TokenSlice, TokenType},
@ -1261,7 +1261,7 @@ fn function_decl(i: &mut TokenSlice) -> PResult<Node<FunctionExpression>> {
fn return_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
colon(i)?;
ignore_whitespace(i);
argument_type(i)
type_(i)
}
let open = open_paren(i)?;
@ -2013,7 +2013,7 @@ fn expression_but_not_pipe(i: &mut TokenSlice) -> PResult<Expr> {
.context(expected("a KCL value"))
.parse_next(i)?;
let ty = opt((colon, opt(whitespace), argument_type)).parse_next(i)?;
let ty = opt((colon, opt(whitespace), type_)).parse_next(i)?;
if let Some((_, _, ty)) = ty {
expr = Expr::AscribedExpression(Box::new(AscribedExpression::new(expr, ty)))
}
@ -2083,7 +2083,7 @@ fn possible_operands(i: &mut TokenSlice) -> PResult<Expr> {
))
.parse_next(i)?;
let ty = opt((colon, opt(whitespace), argument_type)).parse_next(i)?;
let ty = opt((colon, opt(whitespace), type_)).parse_next(i)?;
if let Some((_, _, ty)) = ty {
expr = Expr::AscribedExpression(Box::new(AscribedExpression::new(expr, ty)))
}
@ -2233,7 +2233,21 @@ fn ty_decl(i: &mut TokenSlice) -> PResult<BoxNode<TypeDeclaration>> {
let start = visibility_token.map(|t| t.start).unwrap_or_else(|| decl_token.start);
whitespace(i)?;
let name = identifier(i)?;
let name = alt((
fun.map(|t| {
Node::new(
Identifier {
name: "fn".to_owned(),
digest: None,
},
t.start,
t.end,
t.module_id,
)
}),
identifier,
))
.parse_next(i)?;
let mut end = name.end;
let args = if peek((opt(whitespace), open_paren)).parse_next(i).is_ok() {
@ -2253,7 +2267,7 @@ fn ty_decl(i: &mut TokenSlice) -> PResult<BoxNode<TypeDeclaration>> {
ignore_whitespace(i);
equals(i)?;
ignore_whitespace(i);
let ty = argument_type(i)?;
let ty = type_(i)?;
ParseContext::warn(CompilationError::err(
ty.as_source_range(),
@ -2755,12 +2769,8 @@ fn labeled_argument(i: &mut TokenSlice) -> PResult<LabeledArg> {
.parse_next(i)
}
/// A type of a function argument.
/// This can be:
/// - a primitive type, e.g. `number` or `string` or `bool`
/// - an array type, e.g. `[number]` or `[string]` or `[bool]`
/// - an object type, e.g. `{x: number, y: number}` or `{name: string, age: number}`
fn argument_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
/// Parse a type in various positions.
fn type_(i: &mut TokenSlice) -> PResult<Node<Type>> {
let type_ = alt((
// Object types
// TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`.
@ -2800,14 +2810,69 @@ fn argument_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
}
fn primitive_type(i: &mut TokenSlice) -> PResult<Node<PrimitiveType>> {
let ident = identifier(i)?;
alt((
// A function type: `fn` (`(` type?, (id: type,)* `)` (`:` type)?)?
(
fun,
opt((
// `(` type?, (id: type,)* `)`
delimited(
open_paren,
opt(alt((
// type, (id: type,)+
(
type_,
comma,
opt(whitespace),
separated(
1..,
(identifier, colon, opt(whitespace), type_).map(|(id, _, _, ty)| (id, ty)),
comma_sep,
),
)
.map(|(t, _, _, args)| (Some(t), args)),
// (id: type,)+
separated(
1..,
(identifier, colon, opt(whitespace), type_).map(|(id, _, _, ty)| (id, ty)),
comma_sep,
)
.map(|args| (None, args)),
// type
type_.map(|t| (Some(t), Vec::new())),
))),
close_paren,
),
// `:` type
opt((colon, opt(whitespace), type_)),
)),
)
.map(|(t, tys)| {
let mut ft = FunctionType::empty_fn_type();
let suffix = opt(delimited(open_paren, uom_for_type, close_paren)).parse_next(i)?;
if let Some((args, ret)) = tys {
if let Some((unnamed, named)) = args {
if let Some(unnamed) = unnamed {
ft.unnamed_arg = Some(Box::new(unnamed));
}
ft.named_args = named;
}
if let Some((_, _, ty)) = ret {
ft.return_type = Some(Box::new(ty));
}
}
let mut result = Node::new(PrimitiveType::Boolean, ident.start, ident.end, ident.module_id);
result.inner = PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named(ident));
Ok(result)
Node::new(PrimitiveType::Function(ft), t.start, t.end, t.module_id)
}),
// A named type, possibly with a numeric suffix.
(identifier, opt(delimited(open_paren, uom_for_type, close_paren))).map(|(ident, suffix)| {
let mut result = Node::new(PrimitiveType::Boolean, ident.start, ident.end, ident.module_id);
result.inner =
PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named(ident));
result
}),
))
.parse_next(i)
}
fn array_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
@ -2817,7 +2882,7 @@ fn array_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
}
open_bracket(i)?;
let ty = argument_type(i)?;
let ty = type_(i)?;
let len = opt((
semi_colon,
opt_whitespace,
@ -2905,7 +2970,7 @@ fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"),
opt(question_mark),
opt(whitespace),
opt((colon, opt(whitespace), argument_type).map(|tup| tup.2)),
opt((colon, opt(whitespace), type_).map(|tup| tup.2)),
opt(whitespace),
opt((equals, opt(whitespace), literal).map(|(_, _, literal)| literal)),
)
@ -4777,6 +4842,20 @@ let myBox = box(p=[0,0], h=-3, l=-16, w=-10)
assert_no_err(some_program_string);
}
#[test]
fn parse_function_types() {
let code = r#"foo = x: fn
foo = x: fn(number)
fn foo(x: fn(): number): fn { return 0 }
fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn { return 0 }
type fn
type foo = fn
type foo = fn(a: string, b: { f: fn(): any })
type foo = fn([fn])
type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
"#;
assert_no_err(code);
}
#[test]
fn test_parse_tag_starting_with_bang() {
let some_program_string = r#"startSketchOn(XY)
|> startProfile(at = [0, 0])

View File

@ -278,6 +278,7 @@ async fn inner_line(
end_absolute,
end,
tag,
relative_name: "end",
},
exec_state,
args,
@ -290,6 +291,7 @@ struct StraightLineParams {
end_absolute: Option<[TyF64; 2]>,
end: Option<[TyF64; 2]>,
tag: Option<TagNode>,
relative_name: &'static str,
}
impl StraightLineParams {
@ -299,6 +301,7 @@ impl StraightLineParams {
tag,
end: Some(p),
end_absolute: None,
relative_name: "end",
}
}
fn absolute(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
@ -307,6 +310,7 @@ impl StraightLineParams {
tag,
end: None,
end_absolute: Some(p),
relative_name: "end",
}
}
}
@ -317,6 +321,7 @@ async fn straight_line(
end,
end_absolute,
tag,
relative_name,
}: StraightLineParams,
exec_state: &mut ExecState,
args: Args,
@ -335,7 +340,7 @@ async fn straight_line(
(None, None) => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: "You must supply either `end` or `endAbsolute` arguments".to_owned(),
message: format!("You must supply either `{relative_name}` or `endAbsolute` arguments"),
}));
}
};
@ -447,6 +452,7 @@ async fn inner_x_line(
end_absolute: end_absolute.map(|x| [x, from.into_y()]),
end: length.map(|x| [x, TyF64::new(0.0, NumericType::mm())]),
tag,
relative_name: "length",
},
exec_state,
args,
@ -512,6 +518,7 @@ async fn inner_y_line(
end_absolute: end_absolute.map(|y| [from.into_x(), y]),
end: length.map(|y| [TyF64::new(0.0, NumericType::mm()), y]),
tag,
relative_name: "length",
},
exec_state,
args,

View File

@ -2575,6 +2575,27 @@ sketch002 = startSketchOn({
assert_eq!(actual, input);
}
#[test]
fn recast_function_types() {
let input = r#"foo = x: fn
foo = x: fn(number)
fn foo(x: fn(): number): fn {
return 0
}
fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
return 0
}
type fn
type foo = fn
type foo = fn(a: string, b: { f: fn(): any })
type foo = fn([fn])
type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, input);
}
#[test]
fn unparse_call_inside_function_args_multiple_lines() {
let input = r#"fn foo() {

View File

@ -40,7 +40,7 @@ export fn map(
/// Input array. The output array is this input array, but every element has had the function `f` run on it.
@array: [any],
/// A function. The output array is just the input array, but `f` has been run on every item.
f: Fn,
f: fn(any): any,
): [any] {}
/// Take a starting value. Then, for each element of an array, calculate the next value,
@ -132,7 +132,7 @@ export fn reduce(
/// The first time `f` is run, it will be called with the first item of `array` and this initial starting value.
initial: any,
/// Run once per item in the input `array`. This function takes an item from the array, and the previous output from `f` (or `initial` on the very first run). The final time `f` is run, its output is returned as the final output from `reduce`.
f: Fn,
f: fn(any, accum: any): any,
): [any] {}
/// Append an element to the end of an array.

View File

@ -169,7 +169,7 @@ export type ImportedGeometry
/// The type of any function in KCL.
@(impl = primitive)
export type Fn
export type fn
/// An abstract plane.
///

View File

@ -4,7 +4,8 @@ description: Error from executing argument_error.kcl
---
KCL Semantic error
× semantic: f requires a value with type `Fn`, but found array (list)
× semantic: f requires a value with type `fn(any): any`, but found array
│ (list)
╭─[5:1]
4 │
5 │ map(f, f = [0, 1])
@ -14,7 +15,8 @@ KCL Semantic error
╰────
╰─▶ KCL Semantic error
× semantic: f requires a value with type `Fn`, but found array (list)
× semantic: f requires a value with type `fn(any): any`, but found
│ array (list)
╭─[5:12]
4 │
5 │ map(f, f = [0, 1])

View File

@ -709,7 +709,6 @@ flowchart LR
115 --- 179
115 x--> 228
115 --- 256
115 x--> 307
115 --- 308
164 <--x 116
116 --- 184

View File

@ -1,6 +1,6 @@
import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { CustomIcon } from '@src/components/CustomIcon'
import type { Command } from '@src/lib/commandTypes'
@ -21,13 +21,17 @@ function CommandComboBox({
const defaultOption =
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
// sort disabled commands to the bottom
const sortedOptions = options
.map((command) => ({
command,
disabled: optionIsDisabled(command),
}))
.sort(sortCommands)
.map(({ command }) => command)
const sortedOptions = useMemo(
() =>
options
.map((command) => ({
command,
disabled: optionIsDisabled(command),
}))
.sort(sortCommands)
.map(({ command }) => command),
[options]
)
const fuse = new Fuse(sortedOptions, {
keys: ['displayName', 'name', 'description'],
@ -38,7 +42,7 @@ function CommandComboBox({
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : sortedOptions)
}, [query])
}, [query, sortedOptions])
function handleSelection(command: Command) {
commandBarActor.send({ type: 'Select command', data: { command } })
@ -122,10 +126,18 @@ function CommandComboBox({
export default CommandComboBox
/**
* If the command is configured to be disabled,
* or it is powered by a state machine and the event it is
* associated with is unavailable, disabled it.
*/
function optionIsDisabled(option: Command): boolean {
return (
'machineActor' in option &&
option.machineActor !== undefined &&
!getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name)
option.disabled ||
('machineActor' in option &&
option.machineActor !== undefined &&
!getActorNextEvents(option.machineActor.getSnapshot()).includes(
option.name
))
)
}

View File

@ -261,7 +261,14 @@ export const EngineStream = (props: {
)
.catch(trap)
}
}, [file?.path])
/**
* Watch file not file?.path. Watching the object allows us to send the same file.path back to back
* and still trigger the executeCode() function. JS should not be doing a cache check on the file path
* we should be putting the cache check in Rust.
* e.g. We can call `navigate(/file/<>)` or `navigate(/file/<>/settings)` as much as we want and it will
* trigger this workflow.
*/
}, [file])
const IDLE_TIME_MS = Number(streamIdleMode)

View File

@ -56,7 +56,7 @@ export function LowerRightControls({
Element="externalLink"
to={getReleaseUrl()}
className={
'!no-underline !border-none font-mono text-xs' +
'!no-underline !border-none !bg-transparent font-mono text-xs' +
linkOverrideClassName
}
>

View File

@ -34,7 +34,7 @@ import {
useMenuListener,
useSketchModeMenuEnableDisable,
} from '@src/hooks/useMenu'
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
import useModelingMachineCommands from '@src/hooks/useStateMachineCommands'
import { useKclContext } from '@src/lang/KclProvider'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import {
@ -2091,13 +2091,12 @@ export const ModelingMachineProvider = ({
modelingSend({ type: 'Center camera on selection' })
})
useStateMachineCommands({
useModelingMachineCommands({
machineId: 'modeling',
state: modelingState,
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true,
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
// but we need to support "SketchCancel" and basically
// make this function take the actor or state so it

View File

@ -4,12 +4,16 @@ import {
joinRouterPaths,
joinOSPaths,
safeEncodeForRouterPaths,
webSafePathSplit,
getProjectDirectoryFromKCLFilePath,
} from '@src/lib/paths'
import {
billingActor,
systemIOActor,
useSettings,
useToken,
kclManager,
engineCommandManager,
} from '@src/lib/singletons'
import { BillingTransition } from '@src/machines/billingMachine'
import {
@ -29,6 +33,10 @@ import { useEffect } from 'react'
import { submitAndAwaitTextToKclSystemIO } from '@src/lib/textToCad'
import { reportRejection } from '@src/lib/trap'
import { getUniqueProjectName } from '@src/lib/desktopFS'
import { useLspContext } from '@src/components/LspProvider'
import { useLocation } from 'react-router-dom'
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from '@src/lib/constants'
export function SystemIOMachineLogicListenerDesktop() {
const requestedProjectName = useRequestedProjectName()
@ -40,7 +48,73 @@ export function SystemIOMachineLogicListenerDesktop() {
const requestedTextToCadGeneration = useRequestedTextToCadGeneration()
const token = useToken()
const folders = useFolders()
const { onFileOpen, onFileClose } = useLspContext()
const { pathname } = useLocation()
function safestNavigateToFile({
requestedPath,
requestedFilePathWithExtension,
requestedProjectDirectory,
}: {
requestedPath: string
requestedFilePathWithExtension: string | null
requestedProjectDirectory: string | null
}) {
let filePathWithExtension = null
let projectDirectory = null
// assumes /file/<encodedURIComponent>
// e.g '/file/%2Fhome%2Fkevin-nadro%2FDocuments%2Fzoo-modeling-app-projects%2Fbracket-1%2Fbracket.kcl'
const [iAmABlankString, file, encodedURI] = webSafePathSplit(pathname)
if (
iAmABlankString === '' &&
file === makeUrlPathRelative(PATHS.FILE) &&
encodedURI
) {
filePathWithExtension = decodeURIComponent(encodedURI)
const applicationProjectDirectory = settings.app.projectDirectory.current
projectDirectory = getProjectDirectoryFromKCLFilePath(
filePathWithExtension,
applicationProjectDirectory
)
}
// Close current file in current project if it exists
onFileClose(filePathWithExtension, projectDirectory)
// Open the requested file in the requested project
onFileOpen(requestedFilePathWithExtension, requestedProjectDirectory)
engineCommandManager.rejectAllModelingCommands(
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE
)
/**
* Check that both paths are truthy strings and if they do not match
* then mark it is switchedFiles.
* If they do not match but the origin is falsey we do not want to mark as
* switchedFiles because checkIfSwitchedFilesShouldClear will trigger
* clearSceneAndBustCache if there is a parse error!
*
* i.e. Only do switchedFiles check against two file paths, not null and a file path
*/
if (
filePathWithExtension &&
requestedFilePathWithExtension &&
filePathWithExtension !== requestedFilePathWithExtension
) {
kclManager.switchedFiles = true
}
kclManager.isExecuting = false
navigate(requestedPath)
}
/**
* We watch objects because we want to be able to navigate to itself
* if we used a string the useEffect would not change
* e.g. context.projectName if this was a string we would not be able to
* navigate to CoolProject N times in a row. If we wrap this in an object
* the object is watched not the string value
*/
const useGlobalProjectNavigation = () => {
useEffect(() => {
if (!requestedProjectName.name) {
@ -55,10 +129,21 @@ export function SystemIOMachineLogicListenerDesktop() {
safeEncodeForRouterPaths(projectPathWithoutSpecificKCLFile),
requestedProjectName.subRoute || ''
)
navigate(requestedPath)
safestNavigateToFile({
requestedPath,
requestedFilePathWithExtension: null,
requestedProjectDirectory: projectPathWithoutSpecificKCLFile,
})
}, [requestedProjectName])
}
/**
* We watch objects because we want to be able to navigate to itself
* if we used a string the useEffect would not change
* e.g. context.projectName if this was a string we would not be able to
* navigate to coolFile.kcl N times in a row. If we wrap this in an object
* the object is watched not the string value
*/
const useGlobalFileNavigation = () => {
useEffect(() => {
if (!requestedFileName.file || !requestedFileName.project) {
@ -69,12 +154,20 @@ export function SystemIOMachineLogicListenerDesktop() {
requestedFileName.project,
requestedFileName.file
)
const projectPathWithoutSpecificKCLFile = joinOSPaths(
projectDirectoryPath,
requestedProjectName.name
)
const requestedPath = joinRouterPaths(
PATHS.FILE,
safeEncodeForRouterPaths(filePath),
requestedFileName.subRoute || ''
)
navigate(requestedPath)
safestNavigateToFile({
requestedPath,
requestedFilePathWithExtension: filePath,
requestedProjectDirectory: projectPathWithoutSpecificKCLFile,
})
}, [requestedFileName])
}

View File

@ -332,14 +332,16 @@ export function ToastTextToCadSuccess({
</div>
<div className="flex flex-col justify-between gap-6">
<section>
<h2>Text-to-CAD successful</h2>
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
Prompt: "
<h2 className="font-sans font-bold">Text-to-CAD successful</h2>
<p className="text-sm my-3">
File "{fileName}" was successfully added to project "{projectName}"
from prompt:
</p>
<blockquote className="my-3 border-solid border-l-2 pl-4 text-sm text-chalkboard-70 dark:text-chalkboard-30">
{data.prompt.length > PROMPT_TRUNCATE_LENGTH
? data.prompt.slice(0, PROMPT_TRUNCATE_LENGTH) + '...'
: data.prompt}
"
</p>
</blockquote>
<TextToCadImprovementMessage
className="text-sm mt-2"
label="Not what you expected?"

View File

@ -31,10 +31,16 @@ interface UseStateMachineCommandsArgs<
send: Function
actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean
onCancel?: () => void
}
/**
* @deprecated the type plumbing required for this function is way over-complicated.
* Instead, opt to create `Commands` directly.
*
* This is only used for modelingMachine commands now, and once that is decoupled from React,
* TODO: Delete this function and other state machine helper functions.
*/
export default function useStateMachineCommands<
T extends AnyStateMachine,
S extends StateMachineCommandSetSchema<T>,
@ -44,21 +50,19 @@ export default function useStateMachineCommands<
send,
actor,
commandBarConfig,
allCommandsRequireNetwork = false,
onCancel,
}: UseStateMachineCommandsArgs<T, S>) {
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
const shouldDisableEngineCommands =
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
useEffect(() => {
const disableAllButtons =
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
const newCommands = Object.keys(commandBarConfig || {})
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.flatMap((type) => {
const typeWithProperType = type as EventFrom<T>['type']
return createMachineCommand<T, S>({
@ -70,6 +74,7 @@ export default function useStateMachineCommands<
actor,
commandBarConfig,
onCancel,
forceDisable: shouldDisableEngineCommands,
})
})
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
@ -85,5 +90,5 @@ export default function useStateMachineCommands<
data: { commands: newCommands },
})
}
}, [overallState, isExecuting, isStreamReady])
}, [shouldDisableEngineCommands, commandBarConfig])
}

View File

@ -103,6 +103,7 @@ export type Command<
icon?: Icon
hide?: PLATFORM[number]
hideFromSearch?: boolean
disabled?: boolean
status?: CommandStatus
}

View File

@ -29,6 +29,7 @@ interface CreateMachineCommandProps<
actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void
forceDisable?: boolean
}
// Creates a command with subcommands, ready for use in the CommandBar component,
@ -44,6 +45,7 @@ export function createMachineCommand<
actor,
commandBarConfig,
onCancel,
forceDisable = false,
}: CreateMachineCommandProps<T, S>):
| Command<T, typeof type, S[typeof type]>
| Command<T, typeof type, S[typeof type]>[]
@ -71,6 +73,7 @@ export function createMachineCommand<
actor,
commandBarConfig: recursiveCommandBarConfig,
onCancel,
forceDisable,
})
})
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
@ -105,6 +108,7 @@ export function createMachineCommand<
send({ type })
}
},
disabled: forceDisable,
}
if (commandConfig.args) {

View File

@ -172,6 +172,27 @@ export function getStringAfterLastSeparator(path: string): string {
return path.split(window.electron.sep).pop() || ''
}
/**
* When you have '/home/kevin-nadro/Documents/zoo-modeling-app-projects/bracket-1/bracket.kcl'
* and you need to get the projectDirectory from this string above.
*
* We replace the leading prefix which is the application project directory with
* the empty string. Then it becomes //bracket-1/bracket.kcl
* The first part of the path after the blank / will be the root project directory
*
*/
export function getProjectDirectoryFromKCLFilePath(
path: string,
applicationProjectDirectory: string
): string {
const replacedPath = path.replace(applicationProjectDirectory, '')
const [iAmABlankString, projectDirectory] = desktopSafePathSplit(replacedPath)
if (iAmABlankString === '') {
return projectDirectory
}
return ''
}
/**
* Use this for only web related paths not paths in OS or on disk
* e.g. document.location.pathname

View File

@ -459,35 +459,6 @@ export const commandBarMachine = setup({
Open: {
target: 'Selecting command',
},
'Add commands': {
target: 'Closed',
actions: [
assign({
commands: ({ context, event }) =>
[...context.commands, ...event.data.commands].sort(
sortCommands
),
}),
],
},
'Remove commands': {
target: 'Closed',
actions: [
assign({
commands: ({ context, event }) =>
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
],
},
},
always: {
@ -645,6 +616,29 @@ export const commandBarMachine = setup({
target: '.Command selected',
actions: ['Find and select command', 'Initialize arguments to submit'],
},
'Add commands': {
actions: [
assign({
commands: ({ context, event }) =>
[...context.commands, ...event.data.commands].sort(sortCommands),
}),
],
},
'Remove commands': {
actions: [
assign({
commands: ({ context, event }) =>
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
],
},
},
})

View File

@ -135,6 +135,7 @@ import type { ToolbarModeName } from '@src/lib/toolbar'
import { err, reportRejection, trap } from '@src/lib/trap'
import { uuidv4 } from '@src/lib/utils'
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
import { isDesktop } from '@src/lib/isDesktop'
export type SetSelections =
| {
@ -484,7 +485,9 @@ export const PersistedValues: PersistedKeys[] = ['openPanes']
export const getPersistedContext = (): Partial<PersistedModelingContext> => {
const c = (typeof window !== 'undefined' &&
JSON.parse(localStorage.getItem(PERSIST_MODELING_CONTEXT) || '{}')) || {
openPanes: ['code'],
openPanes: isDesktop()
? (['feature-tree', 'code', 'files'] satisfies Store['openPanes'])
: (['feature-tree', 'code'] satisfies Store['openPanes']),
}
return c
}

View File

@ -191,7 +191,7 @@ export const systemIOMachine = setup({
('error' in event &&
event.error instanceof Error &&
event.error.message) ||
''
'Unknown error in SystemIOMachine'
)
},
[SystemIOMachineActions.setReadWriteProjectDirectory]: assign({

View File

@ -84,6 +84,10 @@ export type SystemIOContext = {
/** has the application gone through the initialization of systemIOMachine at least once.
* this is required to prevent chokidar from spamming invalid events during initialization. */
hasListedProjects: boolean
/**
* We watch objects because we want to be able to navigate to itself
* if we used a string the useEffect would not change
*/
requestedProjectName: { name: string; subRoute?: string }
requestedFileName: { project: string; file: string; subRoute?: string }
canReadWriteProjectDirectory: { value: boolean; error: unknown }