Compare commits

...

16 Commits

Author SHA1 Message Date
0e8d0083c4 Cut release v0.23.1 (#2916)
* Cut release v0.23.1

* Add if to json download
2024-07-05 05:39:17 -04:00
4f4167b247 Rejig state diagram for equipping tools (#2917)
* switch between line and rectangle tool

* disable line tool if rectangle has started

* make rectangle logic clearer from the diagram
2024-07-05 13:40:16 +10:00
fbc2e9d02c Send cancel event from toolbar 'sketch no face' state to enable ESC (#2592)
* Just cancel out of 'sketch no face' state

* add test

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

* trigger ci

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-05 08:53:58 +10:00
33b15e818b fix core dump screenshot part 2 (#2913)
* fix core dump screenshot

* make it robust

* test hardening

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

* trigger CI

* harden test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-05 05:42:54 +10:00
6cebb84ae0 Bump all tauri deps except cli (incl. updater fix) (#2914)
* Bump all tauri deps except cli (incl. updater fix)
Fixes #2741

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

* Trigger CI

* Remove promises from getOsInfo for tauri apis

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-04 09:39:18 -07:00
85403e47e4 Add message "click plane to sketch on" to toolbar after clicking start sketch (#2591)
* Add message to toolbar

"click plane to sketch on"

* Add margin and make the message text smaller

Plus, wrapped it in a div. The spacing and alignment is slightly nicer with the div compared to adding the classes to the List Item element.

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-07-04 16:22:41 +10:00
0dfee64e3b fix core dump screenshot (#2911)
* fix core dump screenshot

* make it robust
2024-07-03 22:58:29 -07:00
6370d45f94 Pause stream when exiting sketch or extruding (#2900)
* Pause when exiting sketch or extruding

* tsc
2024-07-03 22:55:06 -07:00
fb3e922180 Hide the view until the scene is initially built (#2894)
* Hide the view until the scene is initially built

* fmt

* Remove log
2024-07-03 22:40:45 -07:00
1257ec0327 Zoom out on extruded object (#2819) 2024-07-03 22:19:24 -07:00
08e9fe2e52 more codemirror enhancements (#2912)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-03 22:06:52 -07:00
7cec1d45fe Bump html2canvas-pro from 1.5.1 to 1.5.2 (#2908)
Bumps [html2canvas-pro](https://github.com/yorickshan/html2canvas-pro) from 1.5.1 to 1.5.2.
- [Release notes](https://github.com/yorickshan/html2canvas-pro/releases)
- [Changelog](https://github.com/yorickshan/html2canvas-pro/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yorickshan/html2canvas-pro/compare/v1.5.1...v1.5.2)

---
updated-dependencies:
- dependency-name: html2canvas-pro
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 22:03:46 -07:00
93710bc8f2 remove react-codemirror and update all the codemirror libs (#2901)
* start of removing react-codemirror

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

* updates

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

* change theme

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

* updates

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

* updates

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

* updates

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

* disable copilot temporarily

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

* fixes

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-03 21:28:51 -07:00
87e7e9447f cleanup annotations, makes it easier to read (#2905)
ckeanup annotations

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-03 20:59:54 -07:00
8be113d284 update release docs (#2906) 2024-07-04 12:48:08 +10:00
7cfc927d5c Small codemirror changes (#2898)
* Drop unneeded compute indirection in lspAutocompleteKeymapExt

* Dispatch only a single transaction in requestFormatting

Remove addToHistory.of(true), since that is the default.

* Remove old comment and some useless tests

* Just store the view, not the previous viewUpdate, in CompletionRequester

* small codemirror changes from  marijnh

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

* fix some flaky tests

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

* fix

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Marijn Haverbeke <marijn@haverbeke.berlin>
2024-07-03 19:28:46 -07:00
50 changed files with 1139 additions and 1084 deletions

View File

@ -138,6 +138,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files - name: Copy updated .json files
if: github.event_name == 'schedule' if: github.event_name == 'schedule'

View File

@ -124,36 +124,20 @@ Before you submit a contribution PR to this repo, please ensure that:
## Release a new version ## Release a new version
1. Bump the versions in the .json files by creating a `Cut release v{x}.{y}.{z}` PR, committing the changes from 1. Bump the versions by running `./make-realease.sh` while on a fresh pull of main
```bash That will create the branch with the updated json files for you.
VERSION=x.y.z yarn run bump-jsons
```
Alternatively you can try the experimental `make-release.sh` bash script that will create the branch with the updated json files for you.
run `./make-release.sh` for a patch update run `./make-release.sh` for a patch update
run `./make-release.sh "minor"` for minor run `./make-release.sh "minor"` for minor
run `./make-release.sh "major"` for major run `./make-release.sh "major"` for major
The PR may serve as a place to discuss the human-readable changelog and extra QA. A quick way of getting PR's merged since the last bump is to [use this PR filter](https://github.com/KittyCAD/modeling-app/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Amerged+), open up the browser console and paste in the following After it runs you should just need to push the push the branch and open a PR (it will suggest a changelog for you too, delete any that are not user facing)
```typescript The PR may serve as a place to discuss the human-readable changelog and extra QA.
console.log(
'- ' +
Array.from(
document.querySelectorAll('[data-hovercard-type="pull_request"]')
).map((a) => `[${a.innerText}](${a.href})`).join(`
- `)
)
```
grab the md list and delete any that are older than the last bump
2. Merge the PR 2. Merge the PR
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}` 3. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named)
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
## Fuzzing the parser ## Fuzzing the parser

View File

@ -600,7 +600,11 @@ test.describe('Editor tests', () => {
// TODO: Jess needs to fix this but you have to mod the code to get them to show // TODO: Jess needs to fix this but you have to mod the code to get them to show
// up, its an annoying codemirror thing. // up, its an annoying codemirror thing.
await page.locator('.cm-content').click() await page.locator('.cm-content').click()
await page.keyboard.press('End') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
const foldGutterFoldLine = page.locator('[title="Fold line"]') const foldGutterFoldLine = page.locator('[title="Fold line"]')
@ -746,12 +750,12 @@ test.describe('Editor tests', () => {
await page.keyboard.press('ArrowRight') await page.keyboard.press('ArrowRight')
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-info')).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-info') await page.hover('.cm-lint-marker-info')
await expect( await expect(
page.getByText('Identifiers must be lowerCamelCase') page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible() ).toBeVisible()
// select the line that's causing the error and delete it // select the line that's causing the error and delete it
@ -856,16 +860,19 @@ test.describe('Editor tests', () => {
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.type('const topAng = 42') await page.keyboard.type('const topAng = 42')
await page.keyboard.press('ArrowLeft') await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight')
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible() await expect(
page.locator('.cm-lintRange.cm-lintRange-error').first()
).toBeVisible()
await page.locator('.cm-lintRange.cm-lintRange-error').hover() await page.locator('.cm-lintRange.cm-lintRange-error').hover()
await expect(page.locator('.cm-diagnosticText')).toBeVisible() await expect(page.locator('.cm-diagnosticText').first()).toBeVisible()
await expect(page.getByText('Cannot redefine `topAng`')).toBeVisible() await expect(
page.getByText('Cannot redefine `topAng`').first()
).toBeVisible()
const secondTopAng = await page.getByText('topAng').first() const secondTopAng = page.getByText('topAng').first()
await secondTopAng?.dblclick() await secondTopAng?.dblclick()
await page.keyboard.type('otherAng') await page.keyboard.type('otherAng')
@ -929,7 +936,9 @@ test.describe('Editor tests', () => {
// error in gutter // error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.hover('.cm-lint-marker-error:first-child') await page.hover('.cm-lint-marker-error:first-child')
await expect(page.getByText('Expected 2 arguments, got 3')).toBeVisible() await expect(
page.getByText('Expected 2 arguments, got 3').first()
).toBeVisible()
// Make sure there are two diagnostics // Make sure there are two diagnostics
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
@ -965,22 +974,16 @@ test.describe('Editor tests', () => {
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await page.goto('/')
await u.waitForPageLoad()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect( const searchText =
page.getByText(
'sketch profile must lie entirely on one side of the revolution axis' 'sketch profile must lie entirely on one side of the revolution axis'
) await expect(page.getByText(searchText).first()).toBeVisible()
).toBeVisible()
}) })
test.describe('Autocomplete works', () => { test.describe('Autocomplete works', () => {
test('with enter/click to accept the completion', async ({ page }) => { test('with enter/click to accept the completion', async ({ page }) => {
@ -1831,7 +1834,6 @@ test.describe('Copilot ghost text', () => {
// We wanna make sure the code saves. // We wanna make sure the code saves.
await page.waitForTimeout(800) await page.waitForTimeout(800)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible() await expect(page.locator('.cm-ghostText').first()).toBeVisible()
@ -1854,7 +1856,8 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``) // TODO when we make codemirror a widget, we can test this.
//await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('delete in code rejects the suggestion', async ({ page }) => { test('delete in code rejects the suggestion', async ({ page }) => {
@ -2921,7 +2924,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible() await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1]) await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
await expect(page.getByTestId('hover-highlight')).toHaveCount(19) // multiple lines await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines
await page.mouse.move(nothing[0], nothing[1]) await page.mouse.move(nothing[0], nothing[1])
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible() await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
@ -3061,7 +3064,7 @@ const part001 = startSketchOn('XZ')
await page.mouse.move(pos[0], pos[1], { steps: 5 }) await page.mouse.move(pos[0], pos[1], { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toHaveText( await expect(page.getByTestId('hover-highlight').first()).toHaveText(
removeAfterFirstParenthesis(expectedCode) expectedCode
) )
// hover over segment, click it and check the cursor has move to the right place // hover over segment, click it and check the cursor has move to the right place
await page.mouse.click(pos[0], pos[1]) await page.mouse.click(pos[0], pos[1])
@ -3703,9 +3706,7 @@ test.describe('Regression tests', () => {
// Make sure it's not a link // Make sure it's not a link
await expect(zooLogo).not.toHaveAttribute('href') await expect(zooLogo).not.toHaveAttribute('href')
}) })
test.fixme( test('Position _ Is Out Of Range... regression test', async ({ page }) => {
'Position _ Is Out Of Range... regression test',
async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -3724,10 +3725,16 @@ test.describe('Regression tests', () => {
) )
}) })
await u.waitForAuthSkipAppStart() await page.goto('/')
await u.waitForPageLoad()
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
await page.waitForTimeout(200)
// expect it still to be there (sometimes it just clears for a bit?)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 10_000,
})
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
@ -3745,6 +3752,8 @@ test.describe('Regression tests', () => {
await page.keyboard.press('ArrowUp') await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp') await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp') await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('End')
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -3753,9 +3762,10 @@ test.describe('Regression tests', () => {
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.type('thing: "blah"', { delay: 100 }) await page.keyboard.type('thing: "blah"', { delay: 100 })
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.press('ArrowLeft')
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const exampleSketch = startSketchOn("XZ") .toContainText(`const exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> angledLine({ angle: 50, length: 45 }, %) |> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %) |> yLineTo(0, %)
@ -3764,8 +3774,7 @@ test.describe('Regression tests', () => {
thing: "blah"`) thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
} })
)
}) })
test.describe('Sketch tests', () => { test.describe('Sketch tests', () => {
@ -3856,6 +3865,31 @@ test.describe('Sketch tests', () => {
page.getByRole('button', { name: 'Edit Sketch' }) page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible() ).toBeVisible()
}) })
test('Can exit selection of face', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem('persistCode', ``)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByText('click plane or face to sketch on')
).toBeVisible()
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
})
test.describe('Can edit segments by dragging their handles', () => { test.describe('Can edit segments by dragging their handles', () => {
const doEditSegmentsByDraggingHandle = async ( const doEditSegmentsByDraggingHandle = async (
page: Page, page: Page,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 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: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -232,6 +232,7 @@ export async function getUtils(page: Page) {
return { return {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page), waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page),
removeCurrentCode: () => removeCurrentCode(page), removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd), sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => { updateCamPosition: async (xyz: [number, number, number]) => {

View File

@ -1,14 +1,15 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.23.0", "version": "0.23.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.3", "@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0", "@codemirror/commands": "^6.6.0",
"@codemirror/language": "^6.10.2", "@codemirror/language": "^6.10.2",
"@codemirror/lint": "^6.8.1", "@codemirror/lint": "^6.8.1",
"@codemirror/search": "^6.5.6", "@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@csstools/postcss-oklab-function": "^3.0.16", "@csstools/postcss-oklab-function": "^3.0.16",
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2",
@ -19,23 +20,22 @@
"@kittycad/lib": "^0.0.69", "@kittycad/lib": "^0.0.69",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
"@replit/codemirror-interact": "^6.3.1", "@replit/codemirror-interact": "^6.3.1",
"@tauri-apps/api": "2.0.0-beta.12", "@tauri-apps/api": "^2.0.0-beta.14",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2", "@tauri-apps/plugin-dialog": "^2.0.0-beta.6",
"@tauri-apps/plugin-fs": "^2.0.0-beta.5", "@tauri-apps/plugin-fs": "^2.0.0-beta.6",
"@tauri-apps/plugin-http": "^2.0.0-beta.2", "@tauri-apps/plugin-http": "^2.0.0-beta.7",
"@tauri-apps/plugin-os": "^2.0.0-beta.3", "@tauri-apps/plugin-os": "^2.0.0-beta.6",
"@tauri-apps/plugin-process": "^2.0.0-beta.2", "@tauri-apps/plugin-process": "^2.0.0-beta.6",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2", "@tauri-apps/plugin-shell": "^2.0.0-beta.7",
"@tauri-apps/plugin-updater": "^2.0.0-beta.3", "@tauri-apps/plugin-updater": "^2.0.0-beta.6",
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@tweenjs/tween.js": "^23.1.1",
"@uiw/react-codemirror": "^4.21.25",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2", "@xstate/react": "^3.2.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.1", "html2canvas-pro": "^1.5.2",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"re-resizable": "^6.9.11", "re-resizable": "^6.9.11",

View File

@ -1,10 +1,7 @@
import { autocompletion } from '@codemirror/autocomplete' import { foldService } from '@codemirror/language'
import { foldService, syntaxTree } from '@codemirror/language'
import { Extension, EditorState } from '@codemirror/state' import { Extension, EditorState } from '@codemirror/state'
import { ViewPlugin } from '@codemirror/view' import { ViewPlugin } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { import {
docPathFacet, docPathFacet,
LanguageServerPlugin, LanguageServerPlugin,
@ -13,7 +10,6 @@ import {
workspaceFolders, workspaceFolders,
LanguageServerOptions, LanguageServerOptions,
} from './plugin/lsp' } from './plugin/lsp'
import { offsetToPos } from './plugin/util'
export type { LanguageServerClientOptions } from './client' export type { LanguageServerClientOptions } from './client'
export { LanguageServerClient } from './client' export { LanguageServerClient } from './client'
@ -24,14 +20,15 @@ export {
LspWorkerEventType, LspWorkerEventType,
} from './client/codec' } from './client/codec'
export type { LanguageServerOptions } from './plugin/lsp' export type { LanguageServerOptions } from './plugin/lsp'
export type { TransactionInfo, RelevantUpdate } from './plugin/annotations'
export { updateInfo, TransactionAnnotation } from './plugin/annotations'
export { export {
LanguageServerPlugin, LanguageServerPlugin,
LanguageServerPluginSpec, LanguageServerPluginSpec,
docPathFacet, docPathFacet,
languageId, languageId,
workspaceFolders, workspaceFolders,
lspSemanticTokensEvent,
lspDiagnosticsEvent,
lspFormatCodeEvent,
} from './plugin/lsp' } from './plugin/lsp'
export { posToOffset, offsetToPos } from './plugin/util' export { posToOffset, offsetToPos } from './plugin/util'
@ -51,63 +48,10 @@ export function lspPlugin(options: LanguageServerOptions): Extension {
if (plugin == null) return null if (plugin == null) return null
// Get the folding ranges from the language server. // Get the folding ranges from the language server.
// Since this is async we directly need to update the folding ranges after. // Since this is async we directly need to update the folding ranges after.
return plugin?.foldingRange(lineStart, lineEnd) const range = plugin?.foldingRange(lineStart, lineEnd)
return range
}), }),
] ]
if (options.client.getServerCapabilities().completionProvider) {
ext.push(
autocompletion({
defaultKeymap: false,
override: [
async (context) => {
if (plugin === null) {
return null
}
const { state, pos, explicit } = context
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
})
)
}
return ext return ext
} }

View File

@ -1,131 +0,0 @@
import { hasNextSnippetField, pickedCompletion } from '@codemirror/autocomplete'
import { Annotation, Transaction } from '@codemirror/state'
import type { ViewUpdate } from '@codemirror/view'
export enum LspAnnotation {
SemanticTokens = 'semantic-tokens',
FormatCode = 'format-code',
Diagnostics = 'diagnostics',
}
const lspEvent = Annotation.define<LspAnnotation>()
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
export enum TransactionAnnotation {
Remote = 'remote',
UserSelect = 'user.select',
UserInput = 'user.input',
UserMove = 'user.move',
UserDelete = 'user.delete',
UserUndo = 'user.undo',
UserRedo = 'user.redo',
SemanticTokens = 'SemanticTokens',
FormatCode = 'FormatCode',
Diagnostics = 'Diagnostics',
PickedCompletion = 'PickedCompletion',
}
export interface TransactionInfo {
annotations: TransactionAnnotation[]
time: number | null
docChanged: boolean
addToHistory: boolean
inSnippet: boolean
transaction: Transaction
}
export const updateInfo = (update: ViewUpdate): TransactionInfo[] => {
let transactionInfos: TransactionInfo[] = []
for (const tr of update.transactions) {
let annotations: TransactionAnnotation[] = []
if (tr.isUserEvent('select')) {
annotations.push(TransactionAnnotation.UserSelect)
}
if (tr.isUserEvent('input')) {
annotations.push(TransactionAnnotation.UserInput)
}
if (tr.isUserEvent('delete')) {
annotations.push(TransactionAnnotation.UserDelete)
}
if (tr.isUserEvent('undo')) {
annotations.push(TransactionAnnotation.UserUndo)
}
if (tr.isUserEvent('redo')) {
annotations.push(TransactionAnnotation.UserRedo)
}
if (tr.isUserEvent('move')) {
annotations.push(TransactionAnnotation.UserMove)
}
if (tr.annotation(pickedCompletion) !== undefined) {
annotations.push(TransactionAnnotation.PickedCompletion)
}
if (tr.annotation(lspSemanticTokensEvent.type) !== undefined) {
annotations.push(TransactionAnnotation.SemanticTokens)
}
if (tr.annotation(lspFormatCodeEvent.type) !== undefined) {
annotations.push(TransactionAnnotation.FormatCode)
}
if (tr.annotation(lspDiagnosticsEvent.type) !== undefined) {
annotations.push(TransactionAnnotation.Diagnostics)
}
if (tr.annotation(Transaction.remote) !== undefined) {
annotations.push(TransactionAnnotation.Remote)
}
transactionInfos.push({
annotations,
time: tr.annotation(Transaction.time) || null,
docChanged: tr.docChanged,
addToHistory: tr.annotation(Transaction.addToHistory) || false,
inSnippet: hasNextSnippetField(update.state),
transaction: tr,
})
}
return transactionInfos
}
export interface RelevantUpdate {
overall: boolean
userSelect: boolean
time: number | null
}
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
const infos = updateInfo(update)
// Make sure we are not in a snippet
if (infos.some((info) => info.inSnippet)) {
return {
overall: false,
userSelect: false,
time: null,
}
}
return {
overall: infos.some(
(info) =>
info.docChanged ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove)
),
userSelect: infos.some((info) =>
info.annotations.includes(TransactionAnnotation.UserSelect)
),
time: infos.length ? infos[0].time : null,
}
}

View File

@ -1,5 +1,6 @@
import { import {
acceptCompletion, acceptCompletion,
autocompletion,
clearSnippet, clearSnippet,
closeCompletion, closeCompletion,
hasNextSnippetField, hasNextSnippetField,
@ -8,10 +9,17 @@ import {
prevSnippetField, prevSnippetField,
startCompletion, startCompletion,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import { Prec } from '@codemirror/state' import { Prec, Extension } from '@codemirror/state'
import { EditorView, keymap, KeyBinding } from '@codemirror/view' import { EditorView, keymap, KeyBinding, ViewPlugin } from '@codemirror/view'
import { CompletionItemKind } from 'vscode-languageserver-protocol' import {
CompletionItemKind,
CompletionTriggerKind,
} from 'vscode-languageserver-protocol'
import { LanguageServerPlugin } from './lsp'
import { offsetToPos } from './util'
import { syntaxTree } from '@codemirror/language'
export const CompletionItemKindMap = Object.fromEntries( export const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
@ -46,6 +54,59 @@ const lspAutocompleteKeymap: readonly KeyBinding[] = [
}, },
] ]
export const lspAutocompleteKeymapExt = Prec.highest( const lspAutocompleteKeymapExt = Prec.highest(keymap.of(lspAutocompleteKeymap))
keymap.computeN([], () => [lspAutocompleteKeymap])
) export default function lspAutocompleteExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
lspAutocompleteKeymapExt,
autocompletion({
defaultKeymap: false,
override: [
async (context) => {
const { state, pos, explicit, view } = context
let value = view?.plugin(plugin)
if (!value) return null
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
value.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await value.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
]
}

View File

@ -4,7 +4,13 @@ import type {
CompletionResult, CompletionResult,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete' import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
import { Facet, StateEffect, Extension, Transaction } from '@codemirror/state' import {
Facet,
StateEffect,
Extension,
Transaction,
Annotation,
} from '@codemirror/state'
import type { import type {
ViewUpdate, ViewUpdate,
PluginValue, PluginValue,
@ -22,15 +28,10 @@ import {
import { URI } from 'vscode-uri' import { URI } from 'vscode-uri'
import { LanguageServerClient } from '../client' import { LanguageServerClient } from '../client'
import {
lspSemanticTokensEvent,
lspFormatCodeEvent,
relevantUpdate,
} from './annotations'
import { CompletionItemKindMap } from './autocomplete' import { CompletionItemKindMap } from './autocomplete'
import { addToken, SemanticToken } from './semantic-tokens' import { addToken, SemanticToken } from './semantic-tokens'
import { deferExecution, posToOffset, formatMarkdownContents } from './util' import { deferExecution, posToOffset, formatMarkdownContents } from './util'
import { lspAutocompleteKeymapExt } from './autocomplete' import lspAutocompleteExt from './autocomplete'
import lspHoverExt from './hover' import lspHoverExt from './hover'
import lspFormatExt from './format' import lspFormatExt from './format'
import lspIndentExt from './indent' import lspIndentExt from './indent'
@ -47,6 +48,17 @@ export const workspaceFolders = Facet.define<
LSP.WorkspaceFolder[] LSP.WorkspaceFolder[]
>({ combine: useLast }) >({ combine: useLast })
export enum LspAnnotation {
SemanticTokens = 'semantic-tokens',
FormatCode = 'format-code',
Diagnostics = 'diagnostics',
}
const lspEvent = Annotation.define<LspAnnotation>()
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
export interface LanguageServerOptions { export interface LanguageServerOptions {
// We assume this is the main project directory, we are currently working in. // We assume this is the main project directory, we are currently working in.
workspaceFolders: LSP.WorkspaceFolder[] workspaceFolders: LSP.WorkspaceFolder[]
@ -131,11 +143,6 @@ export class LanguageServerPlugin implements PluginValue {
} }
update(viewUpdate: ViewUpdate) { update(viewUpdate: ViewUpdate) {
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
}
// If the doc didn't change we can return early. // If the doc didn't change we can return early.
if (!viewUpdate.docChanged) { if (!viewUpdate.docChanged) {
return return
@ -284,20 +291,17 @@ export class LanguageServerPlugin implements PluginValue {
}, },
}) })
if (!result) return null if (!result || !result.length) return null
for (let i = 0; i < result.length; i++) {
const { range, newText } = result[i]
this.view.dispatch({ this.view.dispatch({
changes: { changes: result.map(({ range, newText }) => ({
from: posToOffset(this.view.state.doc, range.start)!, from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!, to: posToOffset(this.view.state.doc, range.end)!,
insert: newText, insert: newText,
}, })),
annotations: [lspFormatCodeEvent, Transaction.addToHistory.of(true)], annotations: lspFormatCodeEvent,
}) })
} }
}
async requestCompletion( async requestCompletion(
context: CompletionContext, context: CompletionContext,
@ -552,7 +556,7 @@ export class LanguageServerPluginSpec
{ {
provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension { provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension {
return [ return [
lspAutocompleteKeymapExt, lspAutocompleteExt(plugin),
lspFormatExt(plugin), lspFormatExt(plugin),
lspHoverExt(plugin), lspHoverExt(plugin),
lspIndentExt(), lspIndentExt(),

View File

@ -4,7 +4,7 @@ import { EditorView, Decoration, DecorationSet } from '@codemirror/view'
import { Tag, tags } from '@lezer/highlight' import { Tag, tags } from '@lezer/highlight'
import { lspSemanticTokensEvent } from './annotations' import { lspSemanticTokensEvent } from './lsp'
export interface SemanticToken { export interface SemanticToken {
from: number from: number

535
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta.13", features = [] } tauri-build = { version = "2.0.0-beta.18", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
@ -20,18 +20,18 @@ kittycad = "0.3.5"
log = "0.4.21" log = "0.4.21"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] } tauri = { version = "2.0.0-beta.23", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" } tauri-plugin-cli = { version = "2.0.0-beta.7" }
tauri-plugin-deep-link = { version = "2.0.0-beta.3" } tauri-plugin-deep-link = { version = "2.0.0-beta.8" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" } tauri-plugin-dialog = { version = "2.0.0-beta.6" }
tauri-plugin-fs = { version = "2.0.0-beta.9" } tauri-plugin-fs = { version = "2.0.0-beta.10" }
tauri-plugin-http = { version = "2.0.0-beta.6" } tauri-plugin-http = { version = "2.0.0-beta.11" }
tauri-plugin-log = { version = "2.0.0-beta.4" } tauri-plugin-log = { version = "2.0.0-beta.7" }
tauri-plugin-os = { version = "2.0.0-beta.2" } tauri-plugin-os = { version = "2.0.0-beta.7" }
tauri-plugin-persisted-scope = { version = "2.0.0-beta.7" } tauri-plugin-persisted-scope = { version = "2.0.0-beta.10" }
tauri-plugin-process = { version = "2.0.0-beta.2" } tauri-plugin-process = { version = "2.0.0-beta.7" }
tauri-plugin-shell = { version = "2.0.0-beta.2" } tauri-plugin-shell = { version = "2.0.0-beta.8" }
tauri-plugin-updater = { version = "2.0.0-beta.4" } tauri-plugin-updater = { version = "2.0.0-beta.9" }
tokio = { version = "1.37.0", features = ["time", "fs", "process"] } tokio = { version = "1.37.0", features = ["time", "fs", "process"] }
toml = "0.8.2" toml = "0.8.2"
url = "2.5.0" url = "2.5.0"

View File

@ -63,16 +63,22 @@
"subcommands": {} "subcommands": {}
}, },
"deep-link": { "deep-link": {
"domains": [ "mobile": [
{ {
"host": "app.zoo.dev" "host": "app.zoo.dev"
} }
],
"desktop": {
"schemes": [
"zoo",
"zoo-modeling-app"
] ]
}
}, },
"shell": { "shell": {
"open": true "open": true
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.23.0" "version": "0.23.1"
} }

View File

@ -25,7 +25,6 @@ import ModalContainer from 'react-modal-promise'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import Gizmo from 'components/Gizmo' import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { useAppState } from 'AppState'
export function App() { export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS') useRefreshSettings(paths.FILE + 'SETTINGS')
@ -45,17 +44,12 @@ export function App() {
useHotKeyListener() useHotKeyListener()
const { context } = useModelingContext() const { context } = useModelingContext()
const { setAppState } = useAppState()
useEffect(() => {
setAppState({ htmlRef: ref })
}, [ref])
const { auth, settings } = useSettingsAuthContext() const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, ref, token), () => new CoreDumpManager(engineCommandManager, token),
[] []
) )

View File

@ -10,12 +10,10 @@ Please do not fill this up with junk.
interface AppState { interface AppState {
isStreamReady: boolean isStreamReady: boolean
htmlRef: React.RefObject<HTMLDivElement> | null
setAppState: (newAppState: Partial<AppState>) => void setAppState: (newAppState: Partial<AppState>) => void
} }
const AppStateContext = createContext<AppState>({ const AppStateContext = createContext<AppState>({
htmlRef: null,
isStreamReady: false, isStreamReady: false,
setAppState: () => {}, setAppState: () => {},
}) })
@ -24,7 +22,6 @@ export const useAppState = () => useContext(AppStateContext)
export const AppStateProvider = ({ children }: { children: ReactNode }) => { export const AppStateProvider = ({ children }: { children: ReactNode }) => {
const [appState, _setAppState] = useState<AppState>({ const [appState, _setAppState] = useState<AppState>({
htmlRef: null,
isStreamReady: false, isStreamReady: false,
setAppState: () => {}, setAppState: () => {},
}) })
@ -34,7 +31,6 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
return ( return (
<AppStateContext.Provider <AppStateContext.Provider
value={{ value={{
htmlRef: appState.htmlRef,
isStreamReady: appState.isStreamReady, isStreamReady: appState.isStreamReady,
setAppState, setAppState,
}} }}

View File

@ -40,7 +40,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
import { useMemo } from 'react' import { useMemo } from 'react'
import { AppStateProvider, useAppState } from 'AppState' import { AppStateProvider } from 'AppState'
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -180,9 +180,8 @@ export const Router = () => {
function CoreDump() { function CoreDump() {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const { htmlRef } = useAppState()
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, htmlRef, token), () => new CoreDumpManager(engineCommandManager, token),
[] []
) )
useHotkeyWrapper(['meta + shift + .'], () => { useHotkeyWrapper(['meta + shift + .'], () => {

View File

@ -12,6 +12,10 @@ import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { useAppState } from 'AppState' import { useAppState } from 'AppState'
import {
canRectangleTool,
isEditingExistingSketch,
} from 'machines/modelingMachine'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -46,29 +50,48 @@ export function Toolbar({
isExecuting || isExecuting ||
!isStreamReady !isStreamReady
const disableLineButton =
state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
disableAllButtons
useHotkeys( useHotkeys(
'l', 'l',
() => () =>
state.matches('Sketch.Line tool') state.matches('Sketch.Line tool')
? send('CancelSketch') ? send('CancelSketch')
: send('Equip Line tool'), : send({
{ enabled: !disableAllButtons, scopes: ['sketch'] } type: 'change tool',
data: 'line',
}),
{ enabled: !disableLineButton, scopes: ['sketch'] }
) )
const disableTangentialArc =
(!isEditingExistingSketch(context) &&
!state.matches('Sketch.Tangential arc to')) ||
disableAllButtons
useHotkeys( useHotkeys(
'a', 'a',
() => () =>
state.matches('Sketch.Tangential arc to') state.matches('Sketch.Tangential arc to')
? send('CancelSketch') ? send('CancelSketch')
: send('Equip tangential arc to'), : send({
{ enabled: !disableAllButtons, scopes: ['sketch'] } type: 'change tool',
data: 'tangentialArc',
}),
{ enabled: !disableTangentialArc, scopes: ['sketch'] }
) )
const disableRectangle =
(!canRectangleTool(context) && !state.matches('Sketch.Rectangle tool')) ||
disableAllButtons
useHotkeys( useHotkeys(
'r', 'r',
() => () =>
state.matches('Sketch.Rectangle tool') state.matches('Sketch.Rectangle tool')
? send('CancelSketch') ? send('CancelSketch')
: send('Equip rectangle tool'), : send({
{ enabled: !disableAllButtons, scopes: ['sketch'] } type: 'change tool',
data: 'rectangle',
}),
{ enabled: !disableRectangle, scopes: ['sketch'] }
) )
useHotkeys( useHotkeys(
's', 's',
@ -81,7 +104,7 @@ export function Toolbar({
useHotkeys( useHotkeys(
'esc', 'esc',
() => () =>
state.matches('Sketch.SketchIdle') ['Sketch no face', 'Sketch.SketchIdle'].some(state.matches)
? send('Cancel') ? send('Cancel')
: send('CancelSketch'), : send('CancelSketch'),
{ enabled: !disableAllButtons, scopes: ['sketch'] } { enabled: !disableAllButtons, scopes: ['sketch'] }
@ -224,6 +247,11 @@ export function Toolbar({
</ActionButton> </ActionButton>
</li> </li>
)} )}
{state.matches('Sketch no face') && (
<li className="contents">
<div className="mx-2 text-sm">click plane or face to sketch on</div>
</li>
)}
{state.matches('Sketch') && !state.matches('idle') && ( {state.matches('Sketch') && !state.matches('idle') && (
<> <>
<li className="contents" key="line-button"> <li className="contents" key="line-button">
@ -233,7 +261,10 @@ export function Toolbar({
onClick={() => onClick={() =>
state?.matches('Sketch.Line tool') state?.matches('Sketch.Line tool')
? send('CancelSketch') ? send('CancelSketch')
: send('Equip Line tool') : send({
type: 'change tool',
data: 'line',
})
} }
aria-pressed={state?.matches('Sketch.Line tool')} aria-pressed={state?.matches('Sketch.Line tool')}
iconStart={{ iconStart={{
@ -241,7 +272,7 @@ export function Toolbar({
iconClassName, iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons} disabled={disableLineButton}
> >
Line Line
<Tooltip <Tooltip
@ -260,7 +291,10 @@ export function Toolbar({
onClick={() => onClick={() =>
state.matches('Sketch.Tangential arc to') state.matches('Sketch.Tangential arc to')
? send('CancelSketch') ? send('CancelSketch')
: send('Equip tangential arc to') : send({
type: 'change tool',
data: 'tangentialArc',
})
} }
aria-pressed={state.matches('Sketch.Tangential arc to')} aria-pressed={state.matches('Sketch.Tangential arc to')}
iconStart={{ iconStart={{
@ -268,11 +302,7 @@ export function Toolbar({
iconClassName, iconClassName,
bgClassName, bgClassName,
}} }}
disabled={ disabled={disableTangentialArc}
(!state.can('Equip tangential arc to') &&
!state.matches('Sketch.Tangential arc to')) ||
disableAllButtons
}
> >
Tangential Arc Tangential Arc
<Tooltip <Tooltip
@ -291,7 +321,10 @@ export function Toolbar({
onClick={() => onClick={() =>
state.matches('Sketch.Rectangle tool') state.matches('Sketch.Rectangle tool')
? send('CancelSketch') ? send('CancelSketch')
: send('Equip rectangle tool') : send({
type: 'change tool',
data: 'rectangle',
})
} }
aria-pressed={state.matches('Sketch.Rectangle tool')} aria-pressed={state.matches('Sketch.Rectangle tool')}
iconStart={{ iconStart={{
@ -299,13 +332,9 @@ export function Toolbar({
iconClassName, iconClassName,
bgClassName, bgClassName,
}} }}
disabled={ disabled={disableRectangle}
(!state.can('Equip rectangle tool') &&
!state.matches('Sketch.Rectangle tool')) ||
disableAllButtons
}
title={ title={
state.can('Equip rectangle tool') canRectangleTool(context)
? 'Rectangle' ? 'Rectangle'
: 'Can only be used when a sketch is empty currently' : 'Can only be used when a sketch is empty currently'
} }

View File

@ -804,7 +804,7 @@ export class SceneEntities {
// Update the primary AST and unequip the rectangle tool // Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'CancelSketch' }) sceneInfra.modelingSend({ type: 'Finish rectangle' })
const { programMemory } = await executeAst({ const { programMemory } = await executeAst({
ast: _ast, ast: _ast,

View File

@ -1,5 +1,6 @@
import { Completion } from '@codemirror/autocomplete' import { Completion } from '@codemirror/autocomplete'
import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror' import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { CustomIcon } from 'components/CustomIcon' import { CustomIcon } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -12,6 +13,7 @@ import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styles from './CommandBarKclInput.module.css' import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst' import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
function CommandBarKclInput({ function CommandBarKclInput({
arg, arg,
@ -63,9 +65,7 @@ function CommandBarKclInput({
const { setContainer } = useCodeMirror({ const { setContainer } = useCodeMirror({
container: editorRef.current, container: editorRef.current,
value, initialDocValue: value,
indentWithTab: false,
basicSetup: false,
autoFocus: true, autoFocus: true,
selection: { selection: {
anchor: 0, anchor: 0,
@ -74,7 +74,6 @@ function CommandBarKclInput({
? previouslySetValue.valueText.length ? previouslySetValue.valueText.length
: defaultValue.length, : defaultValue.length,
}, },
accessKey: 'command-bar',
theme: theme:
settings.context.app.theme.current === 'system' settings.context.app.theme.current === 'system'
? getSystemTheme() ? getSystemTheme()
@ -96,8 +95,12 @@ function CommandBarKclInput({
} }
return tr return tr
}), }),
EditorView.updateListener.of((vu: ViewUpdate) => {
if (vu.docChanged) {
setValue(vu.state.doc.toString())
}
}),
], ],
onChange: (newValue) => setValue(newValue),
}) })
useEffect(() => { useEffect(() => {

View File

@ -175,7 +175,11 @@ const FileTreeItem = ({
codeManager.code codeManager.code
) )
codeManager.writeToFile() codeManager.writeToFile()
kclManager.executeCode(true, true)
kclManager.isFirstRender = true
kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
} else { } else {
// Let the lsp servers know we closed a file. // Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null) onFileClose(currentFile?.path || null, project?.path || null)

View File

@ -64,7 +64,7 @@ import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@uiw/react-codemirror' import { EditorSelection, Transaction } from '@codemirror/state'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards' import { getVarNameModal } from 'hooks/useToolbarGuards'
@ -128,7 +128,7 @@ export const ModelingMachineProvider = ({
'enable copilot': () => { 'enable copilot': () => {
editorManager.setCopilotEnabled(true) editorManager.setCopilotEnabled(true)
}, },
'sketch exit execute': () => { 'sketch exit execute': ({ store }) => {
;(async () => { ;(async () => {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine() await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
@ -162,7 +162,10 @@ export const ModelingMachineProvider = ({
}) })
} }
kclManager.executeCode(true) store.videoElement?.pause()
kclManager.executeCode(true).then(() => {
store.videoElement?.play()
})
})() })()
}, },
'Set mouse state': assign({ 'Set mouse state': assign({
@ -438,17 +441,6 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length <= 0) return false if (selectionRanges.codeBasedSelections.length <= 0) return false
return true return true
}, },
'Sketch is empty': ({ sketchDetails }) => {
const node = getNodeFromPath<VariableDeclaration>(
kclManager.ast,
sketchDetails?.sketchPathToNode || [],
'VariableDeclaration'
)
// This should not be returning false, and it should be caught
// but we need to simulate old behavior to move on.
if (err(node)) return false
return node.node?.declarations?.[0]?.init.type !== 'PipeExpression'
},
'Selection is on face': ({ selectionRanges }, { data }) => { 'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))

View File

@ -0,0 +1,171 @@
import React, {
useEffect,
useMemo,
useRef,
useState,
forwardRef,
useImperativeHandle,
} from 'react'
import {
EditorState,
EditorStateConfig,
Extension,
StateEffect,
} from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { oneDark } from '@codemirror/theme-one-dark'
//reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
const useFirstRender = () => {
const firstRender = useRef(true)
useEffect(() => {
firstRender.current = false
}, [])
return firstRender.current
}
const defaultLightThemeOption = EditorView.theme(
{
'&': {
backgroundColor: '#fff',
},
},
{
dark: false,
}
)
interface CodeEditorRef {
editor?: HTMLDivElement | null
view?: EditorView
state?: EditorState
}
interface CodeEditorProps {
onCreateEditor?: (view: EditorView | null) => void
initialDocValue?: EditorStateConfig['doc']
extensions?: Extension
theme: 'light' | 'dark'
autoFocus?: boolean
selection?: EditorStateConfig['selection']
}
interface UseCodeMirror extends CodeEditorProps {
container?: HTMLDivElement | null
}
const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
const {
onCreateEditor,
extensions = [],
initialDocValue,
theme,
autoFocus = false,
selection,
} = props
const editor = useRef<HTMLDivElement>(null)
const { view, state, container } = useCodeMirror({
container: editor.current,
onCreateEditor,
extensions,
initialDocValue,
theme,
autoFocus,
selection,
})
useImperativeHandle(
ref,
() => ({ editor: editor.current, view: view, state: state }),
[editor, container, view, state]
)
return <div ref={editor}></div>
})
export function useCodeMirror(props: UseCodeMirror) {
const {
onCreateEditor,
extensions = [],
initialDocValue,
theme,
autoFocus = false,
selection,
} = props
const [container, setContainer] = useState<HTMLDivElement | null>()
const [view, setView] = useState<EditorView>()
const [state, setState] = useState<EditorState>()
const isFirstRender = useFirstRender()
const targetExtensions = useMemo(() => {
let exts = Array.isArray(extensions) ? extensions : []
if (theme === 'dark') {
exts = [...exts, oneDark]
} else if (theme === 'light') {
exts = [...exts, defaultLightThemeOption]
}
return exts
}, [extensions, theme])
useEffect(() => {
if (container && !state) {
const config = {
doc: initialDocValue,
selection,
extensions: [...Array.of(extensions)],
}
const stateCurrent = EditorState.create(config)
setState(stateCurrent)
if (!view) {
const viewCurrent = new EditorView({
state: stateCurrent,
parent: container,
})
setView(viewCurrent)
onCreateEditor && onCreateEditor(viewCurrent)
}
}
return () => {
if (view) {
setState(undefined)
setView(undefined)
}
}
}, [container, state])
useEffect(() => setContainer(props.container), [props.container])
useEffect(
() => () => {
if (view) {
view.destroy()
setView(undefined)
}
},
[view]
)
useEffect(() => {
if (autoFocus && view) {
view.focus()
}
}, [autoFocus, view])
useEffect(() => {
if (view && !isFirstRender) {
view.dispatch({
effects: StateEffect.reconfigure.of(targetExtensions),
})
}
}, [targetExtensions])
return { view, setView, container, setContainer, state, setState }
}
export default CodeEditor

View File

@ -1,4 +1,3 @@
import ReactCodeMirror from '@uiw/react-codemirror'
import { TEST } from 'env' import { TEST } from 'env'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
@ -43,6 +42,7 @@ import {
closeBracketsKeymap, closeBracketsKeymap,
completionKeymap, completionKeymap,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import CodeEditor from './CodeEditor'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -185,15 +185,15 @@ export const KclEditorPane = () => {
id="code-mirror-override" id="code-mirror-override"
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')} className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
> >
<ReactCodeMirror <CodeEditor
value={initialCode.current} initialDocValue={initialCode.current}
extensions={editorExtensions} extensions={editorExtensions}
theme={theme} theme={theme}
onCreateEditor={(_editorView) => { onCreateEditor={(_editorView) => {
if (_editorView === null) return
editorManager.setEditorView(_editorView) editorManager.setEditorView(_editorView)
}} }}
indentWithTab={false}
basicSetup={false}
/> />
</div> </div>
) )

View File

@ -6,14 +6,12 @@ import React, { useMemo } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAppState } from 'AppState'
export const RefreshButton = ({ children }: React.PropsWithChildren) => { export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const { htmlRef } = useAppState()
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, htmlRef, token), () => new CoreDumpManager(engineCommandManager, token),
[] []
) )

View File

@ -175,7 +175,12 @@ export const SettingsAuthProviderBase = ({
id: `${event.type}.success`, id: `${event.type}.success`,
}) })
}, },
'Execute AST': () => kclManager.executeCode(true, true), 'Execute AST': () => {
kclManager.isFirstRender = true
kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
}, },
services: { services: {
'Persist settings': (context) => 'Persist settings': (context) =>

View File

@ -8,9 +8,11 @@ import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { butName } from 'lib/cameraControls' import { butName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections' import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager } from 'lib/singletons'
export const Stream = () => { export const Stream = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isFirstRender, setIsFirstRender] = useState(kclManager.isFirstRender)
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>() const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
@ -53,6 +55,10 @@ export const Stream = () => {
}) })
}, []) }, [])
useEffect(() => {
setIsFirstRender(kclManager.isFirstRender)
}, [kclManager.isFirstRender])
useEffect(() => { useEffect(() => {
if ( if (
typeof window === 'undefined' || typeof window === 'undefined' ||
@ -62,6 +68,13 @@ export const Stream = () => {
if (!videoRef.current) return if (!videoRef.current) return
if (!context.store?.mediaStream) return if (!context.store?.mediaStream) return
videoRef.current.srcObject = context.store.mediaStream videoRef.current.srcObject = context.store.mediaStream
send({
type: 'Set context',
data: {
videoElement: videoRef.current,
},
})
}, [context.store?.mediaStream]) }, [context.store?.mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
@ -166,10 +179,14 @@ export const Stream = () => {
</Loading> </Loading>
</div> </div>
)} )}
{isLoading && ( {(isLoading || isFirstRender) && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"> <div className="text-center absolute inset-0">
<Loading> <Loading>
{!isLoading && isFirstRender ? (
<span data-testid="loading-stream">Building scene...</span>
) : (
<span data-testid="loading-stream">Loading stream...</span> <span data-testid="loading-stream">Loading stream...</span>
)}
</Loading> </Loading>
</div> </div>
)} )}

View File

@ -222,11 +222,7 @@ export default class EditorManager {
return return
} }
const ignoreEvents: ModelingMachineEvent['type'][] = [ const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
'Equip Line tool',
'Equip tangential arc to',
'Equip rectangle tool',
]
if (!this._modelingEvent) { if (!this._modelingEvent) {
return return

View File

@ -23,17 +23,12 @@ import {
} from '@codemirror/state' } from '@codemirror/state'
import { completionStatus } from '@codemirror/autocomplete' import { completionStatus } from '@codemirror/autocomplete'
import { import {
TransactionAnnotation,
offsetToPos, offsetToPos,
posToOffset, posToOffset,
LanguageServerOptions, LanguageServerOptions,
LanguageServerClient, LanguageServerClient,
docPathFacet, docPathFacet,
languageId, languageId,
TransactionInfo,
updateInfo,
RelevantUpdate,
lspPlugin,
} from '@kittycad/codemirror-lsp-client' } from '@kittycad/codemirror-lsp-client'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams' import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
@ -98,11 +93,6 @@ const completionDecoration = StateField.define<CompletionState>({
return state return state
} }
// We only care about transactions with effects.
if (!transaction.effects) {
return state
}
for (const effect of transaction.effects) { for (const effect of transaction.effects) {
if (effect.is(addSuggestion)) { if (effect.is(addSuggestion)) {
// When adding a suggestion, we set th ghostText // When adding a suggestion, we set th ghostText
@ -198,95 +188,68 @@ const completionDecoration = StateField.define<CompletionState>({
), ),
}) })
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
const infos = updateInfo(update)
// Make sure we are not in a snippet
if (infos.some((info: TransactionInfo) => info.inSnippet)) {
return {
overall: false,
userSelect: false,
time: null,
}
}
return {
overall: infos.some(
(info: TransactionInfo) =>
info.transaction.annotation(copilotPluginEvent.type) !== undefined ||
info.annotations.includes(TransactionAnnotation.UserSelect) ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove)
),
userSelect: infos.some((info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect)
),
time: infos.length ? infos[0].time : null,
}
}
// A view plugin that requests completions from the server after a delay // A view plugin that requests completions from the server after a delay
export class CompletionRequester implements PluginValue { export class CompletionRequester implements PluginValue {
private client: LanguageServerClient private client: LanguageServerClient
private lastPos: number = 0 private lastPos: number = 0
private viewUpdate: ViewUpdate | null = null
private queuedUids: string[] = [] private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => { private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.requestCompletions() this.requestCompletions()
}, changesDelay) }, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.rejectSuggestionCommand() this.rejectSuggestionCommand()
}, changesDelay) }, changesDelay)
constructor(client: LanguageServerClient) { constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client this.client = client
} }
update(viewUpdate: ViewUpdate) { update(viewUpdate: ViewUpdate) {
this.viewUpdate = viewUpdate
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
}
// If we have a user select event, we want to clear the ghost text.
if (isRelevant.userSelect) {
this._deffererUserSelect(true)
return
}
// Make sure we are in a state where we can request completions. // Make sure we are in a state where we can request completions.
if (!editorManager.copilotEnabled) { if (!editorManager.copilotEnabled) {
return return
} }
this.lastPos = this.viewUpdate.state.selection.main.head let isUserSelect = false
this._deffererCodeUpdate(true) let isRelevant = false
for (const tr of viewUpdate.transactions) {
if (tr.isUserEvent('select')) {
isUserSelect = true
break
} else if (tr.isUserEvent('input')) {
isRelevant = true
} else if (tr.isUserEvent('delete')) {
isRelevant = true
} else if (tr.isUserEvent('undo')) {
isRelevant = true
} else if (tr.isUserEvent('redo')) {
isRelevant = true
} else if (tr.isUserEvent('move')) {
isRelevant = true
} else if (tr.annotation(copilotPluginEvent.type) !== undefined) {
isRelevant = true
}
}
// If we have a user select event, we want to clear the ghost text.
if (isUserSelect) {
this._deffererUserSelect(true)
return
}
if (!isRelevant) {
return
}
this.lastPos = this.view.state.selection.main.head
if (viewUpdate.docChanged) this._deffererCodeUpdate(true)
} }
ghostText(): GhostText | null { ghostText(): GhostText | null {
if (!this.viewUpdate) { return this.view.state.field(completionDecoration)?.ghostText || null
return null
}
return (
this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null
)
} }
containsGhostText(): boolean { containsGhostText(): boolean {
@ -294,33 +257,23 @@ export class CompletionRequester implements PluginValue {
} }
autocompleting(): boolean { autocompleting(): boolean {
if (!this.viewUpdate) { return completionStatus(this.view.state) === 'active'
return false
}
return completionStatus(this.viewUpdate.state) === 'active'
} }
notFocused(): boolean { notFocused(): boolean {
if (!this.viewUpdate) { return !this.view.hasFocus
return true
}
return !this.viewUpdate.view.hasFocus
} }
async requestCompletions(): Promise<void> { async requestCompletions(): Promise<void> {
if ( if (
this.viewUpdate === null ||
this.containsGhostText() || this.containsGhostText() ||
this.autocompleting() || this.autocompleting() ||
this.notFocused() || this.notFocused()
!this.viewUpdate.docChanged
) { ) {
return return
} }
const pos = this.viewUpdate.state.selection.main.head const pos = this.view.state.selection.main.head
// Check if the position has changed // Check if the position has changed
if (pos !== this.lastPos) { if (pos !== this.lastPos) {
@ -328,7 +281,7 @@ export class CompletionRequester implements PluginValue {
} }
// Get the current position and source // Get the current position and source
const state = this.viewUpdate.state const state = this.view.state
const dUri = state.facet(docPathFacet) const dUri = state.facet(docPathFacet)
// Request completion from the server // Request completion from the server
@ -396,14 +349,14 @@ export class CompletionRequester implements PluginValue {
// Dispatch an effect to add the suggestion // Dispatch an effect to add the suggestion
// If the completion starts before the end of the line, check the end of the line with the end of the completion. // If the completion starts before the end of the line, check the end of the line with the end of the completion.
const line = this.viewUpdate.view.state.doc.lineAt(pos) const line = this.view.state.doc.lineAt(pos)
if (line.to !== pos) { if (line.to !== pos) {
const ending = this.viewUpdate.view.state.doc.sliceString(pos, line.to) const ending = this.view.state.doc.sliceString(pos, line.to)
if (displayText.endsWith(ending)) { if (displayText.endsWith(ending)) {
displayText = displayText.slice(0, displayText.length - ending.length) displayText = displayText.slice(0, displayText.length - ending.length)
} else if (displayText.includes(ending)) { } else if (displayText.includes(ending)) {
// Remove the ending // Remove the ending
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: pos, from: pos,
to: line.to, to: line.to,
@ -416,7 +369,7 @@ export class CompletionRequester implements PluginValue {
} }
} }
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: pos, from: pos,
to: pos, to: pos,
@ -442,10 +395,6 @@ export class CompletionRequester implements PluginValue {
} }
acceptSuggestionCommand(): boolean { acceptSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText() const ghostText = this.ghostText()
if (!ghostText) { if (!ghostText) {
return false return false
@ -463,7 +412,7 @@ export class CompletionRequester implements PluginValue {
const suggestion = ghostText.text const suggestion = ghostText.text
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: ghostTextStart, from: ghostTextStart,
to: ghostTextEnd, to: ghostTextEnd,
@ -475,7 +424,7 @@ export class CompletionRequester implements PluginValue {
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart) const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: actualTextStart, from: actualTextStart,
to: tmpTextEnd, to: tmpTextEnd,
@ -490,10 +439,6 @@ export class CompletionRequester implements PluginValue {
} }
rejectSuggestionCommand(): boolean { rejectSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText() const ghostText = this.ghostText()
if (!ghostText) { if (!ghostText) {
return false return false
@ -503,7 +448,7 @@ export class CompletionRequester implements PluginValue {
const ghostTextStart = ghostText.displayPos const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText const ghostTextEnd = ghostText.endGhostText
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: ghostTextStart, from: ghostTextStart,
to: ghostTextEnd, to: ghostTextEnd,
@ -521,10 +466,6 @@ export class CompletionRequester implements PluginValue {
} }
sameKeyCommand(key: string) { sameKeyCommand(key: string) {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText() const ghostText = this.ghostText()
if (!ghostText) { if (!ghostText) {
return false return false
@ -534,10 +475,10 @@ export class CompletionRequester implements PluginValue {
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress // When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
const ghostTextStart = ghostText.displayPos const ghostTextStart = ghostText.displayPos
const indent = this.viewUpdate.view.state.facet(indentUnit) const indent = this.view.state.facet(indentUnit)
if (key === tabKey && ghostText.displayText.startsWith(indent)) { if (key === tabKey && ghostText.displayText.startsWith(indent)) {
this.viewUpdate.view.dispatch({ this.view.dispatch({
selection: { anchor: ghostTextStart + indent.length }, selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length), effects: typeFirst.of(indent.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
@ -551,7 +492,7 @@ export class CompletionRequester implements PluginValue {
return this.acceptSuggestionCommand() return this.acceptSuggestionCommand()
} else { } else {
// Use this to delete the first letter of the suggestion // Use this to delete the first letter of the suggestion
this.viewUpdate.view.dispatch({ this.view.dispatch({
selection: { anchor: ghostTextStart + 1 }, selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1), effects: typeFirst.of(1),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
@ -598,7 +539,7 @@ export class CompletionRequester implements PluginValue {
export const copilotPlugin = (options: LanguageServerOptions): Extension => { export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: CompletionRequester | null = null let plugin: CompletionRequester | null = null
const completionPlugin = ViewPlugin.define( const completionPlugin = ViewPlugin.define(
(view) => (plugin = new CompletionRequester(options.client)) (view) => (plugin = new CompletionRequester(view, options.client))
) )
const domHandlers = EditorView.domEventHandlers({ const domHandlers = EditorView.domEventHandlers({
@ -625,8 +566,6 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
}) })
const rejectSuggestionCommand = (view: EditorView): boolean => { const rejectSuggestionCommand = (view: EditorView): boolean => {
if (view.plugin === null) return false
// Get the current plugin from the map. // Get the current plugin from the map.
const p = view.plugin(completionPlugin) const p = view.plugin(completionPlugin)
if (p === null) return false if (p === null) return false
@ -681,7 +620,6 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
) )
return [ return [
lspPlugin(options),
completionPlugin, completionPlugin,
copilotAutocompleteKeymapExt, copilotAutocompleteKeymapExt,
domHandlers, domHandlers,

View File

@ -2,12 +2,9 @@ import { Extension } from '@codemirror/state'
import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view' import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view'
import { import {
LanguageServerOptions, LanguageServerOptions,
updateInfo,
TransactionInfo,
RelevantUpdate,
TransactionAnnotation,
LanguageServerClient, LanguageServerClient,
lspPlugin, lspPlugin,
lspFormatCodeEvent,
} from '@kittycad/codemirror-lsp-client' } from '@kittycad/codemirror-lsp-client'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { codeManager, editorManager, kclManager } from 'lib/singletons' import { codeManager, editorManager, kclManager } from 'lib/singletons'
@ -18,34 +15,6 @@ import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecute
const changesDelay = 600 const changesDelay = 600
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
const infos = updateInfo(update)
// Make sure we are not in a snippet
if (infos.some((info: TransactionInfo) => info.inSnippet)) {
return {
overall: false,
userSelect: false,
time: null,
}
}
return {
overall: infos.some(
(info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect) ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove) ||
info.annotations.includes(TransactionAnnotation.FormatCode)
),
userSelect: infos.some((info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect)
),
time: infos.length ? infos[0].time : null,
}
}
// A view plugin that requests completions from the server after a delay // A view plugin that requests completions from the server after a delay
export class KclPlugin implements PluginValue { export class KclPlugin implements PluginValue {
private viewUpdate: ViewUpdate | null = null private viewUpdate: ViewUpdate | null = null
@ -75,18 +44,38 @@ export class KclPlugin implements PluginValue {
this.viewUpdate = viewUpdate this.viewUpdate = viewUpdate
editorManager.setEditorView(viewUpdate.view) editorManager.setEditorView(viewUpdate.view)
const isRelevant = relevantUpdate(viewUpdate) let isUserSelect = false
if (!isRelevant.overall) { let isRelevant = false
return for (const tr of viewUpdate.transactions) {
if (tr.isUserEvent('select')) {
isUserSelect = true
break
} else if (tr.isUserEvent('input')) {
isRelevant = true
} else if (tr.isUserEvent('delete')) {
isRelevant = true
} else if (tr.isUserEvent('undo')) {
isRelevant = true
} else if (tr.isUserEvent('redo')) {
isRelevant = true
} else if (tr.isUserEvent('move')) {
isRelevant = true
} else if (tr.annotation(lspFormatCodeEvent.type)) {
isRelevant = true
}
} }
// If we have a user select event, we want to update what parts are // If we have a user select event, we want to update what parts are
// highlighted. // highlighted.
if (isRelevant.userSelect) { if (isUserSelect) {
this._deffererUserSelect(true) this._deffererUserSelect(true)
return return
} }
if (!isRelevant) {
return
}
if (!viewUpdate.docChanged) { if (!viewUpdate.docChanged) {
return return
} }

View File

@ -46,15 +46,7 @@ class KclLanguage extends Language {
const parser = new KclParser() const parser = new KclParser()
super( super(data, parser, [plugin], 'kcl')
data,
// For now let's use the javascript parser.
// It works really well and has good syntax highlighting.
// We can use our lsp for the rest.
parser,
[plugin],
'kcl'
)
} }
} }

View File

@ -65,7 +65,10 @@ export function useSetupEngineManager(
executeCode: () => { executeCode: () => {
// We only want to execute the code here that we already have set. // We only want to execute the code here that we already have set.
// Nothing else. // Nothing else.
return kclManager.executeCode(true, true) kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
}, },
token, token,
settings, settings,

View File

@ -260,3 +260,8 @@ code {
@apply bg-chalkboard-20 dark:bg-chalkboard-90; @apply bg-chalkboard-20 dark:bg-chalkboard-90;
} }
} }
#code-mirror-override .cm-scroller,
#code-mirror-override .cm-editor {
height: 100% !important;
}

View File

@ -15,6 +15,7 @@ import {
ProgramMemory, ProgramMemory,
recast, recast,
SketchGroup, SketchGroup,
SourceRange,
ExtrudeGroup, ExtrudeGroup,
} from 'lang/wasm' } from 'lang/wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
@ -65,6 +66,8 @@ export class KclManager {
private _wasmInitFailedCallback: (arg: boolean) => void = () => {} private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {} private _executeCallback: () => void = () => {}
isFirstRender = true
get ast() { get ast() {
return this._ast return this._ast
} }
@ -194,7 +197,11 @@ export class KclManager {
async executeAst( async executeAst(
ast: Program = this._ast, ast: Program = this._ast,
zoomToFit?: boolean, zoomToFit?: boolean,
executionId?: number executionId?: number,
zoomOnRangeAndType?: {
range: SourceRange
type: string
}
): Promise<void> { ): Promise<void> {
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
const currentExecutionId = executionId || Date.now() const currentExecutionId = executionId || Date.now()
@ -218,12 +225,20 @@ export class KclManager {
defaultSelectionFilter(programMemory, this.engineCommandManager) defaultSelectionFilter(programMemory, this.engineCommandManager)
if (zoomToFit) { if (zoomToFit) {
let zoomObjectId: string | undefined = ''
if (zoomOnRangeAndType) {
zoomObjectId = this.engineCommandManager?.mapRangeToObjectId(
zoomOnRangeAndType.range,
zoomOnRangeAndType.type
)
}
await this.engineCommandManager.sendSceneCommand({ await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'zoom_to_fit', type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects padding: 0.1, // padding around the objects
}, },
}) })
@ -357,6 +372,11 @@ export class KclManager {
execute: boolean, execute: boolean,
optionalParams?: { optionalParams?: {
focusPath?: PathToNode focusPath?: PathToNode
zoomToFit?: boolean
zoomOnRangeAndType?: {
range: SourceRange
type: string
}
} }
): Promise<{ ): Promise<{
newAst: Program newAst: Program
@ -400,7 +420,12 @@ export class KclManager {
codeManager.updateCodeEditor(newCode) codeManager.updateCodeEditor(newCode)
// Write the file to disk. // Write the file to disk.
await codeManager.writeToFile() await codeManager.writeToFile()
await this.executeAst(astWithUpdatedSource) await this.executeAst(
astWithUpdatedSource,
optionalParams?.zoomToFit,
undefined,
optionalParams?.zoomOnRangeAndType
)
} else { } else {
// When we don't re-execute, we still want to update the program // When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor // memory with the new ast. So we will hit the mock executor

View File

@ -6,7 +6,8 @@ import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs' import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { Annotation, KeyBinding, Transaction } from '@uiw/react-codemirror' import { Annotation, Transaction } from '@codemirror/state'
import { KeyBinding } from '@codemirror/view'
const PERSIST_CODE_KEY = 'persistCode' const PERSIST_CODE_KEY = 'persistCode'

View File

@ -2084,4 +2084,25 @@ export class EngineCommandManager extends EventTarget {
setScaleGridVisibility(visible: boolean) { setScaleGridVisibility(visible: boolean) {
this.modifyGrid(!visible) this.modifyGrid(!visible)
} }
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
// So when passing a range, we need to also specify the command type
mapRangeToObjectId(
range: SourceRange,
commandTypeToTarget: string
): string | undefined {
const values = Object.entries(this.artifactMap)
for (const [id, data] of values) {
if (data.type !== 'result') continue
// Our range selection seems to just select the cursor position, so either
// of these can be right...
if (
(data.range[0] === range[0] || data.range[1] === range[1]) &&
data.commandType === commandTypeToTarget
)
return id
}
return undefined
}
} }

View File

@ -37,21 +37,22 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.', description: 'Enter sketch mode.',
icon: 'sketch', icon: 'sketch',
}, },
'Equip Line tool': { // TODO the event is no 'change tool' with data: 'line', 'rectangle' etc
description: 'Start drawing straight lines.', // 'Equip Line tool': {
icon: 'line', // description: 'Start drawing straight lines.',
displayName: 'Line', // icon: 'line',
}, // displayName: 'Line',
'Equip tangential arc to': { // },
description: 'Start drawing an arc tangent to the current segment.', // 'Equip tangential arc to': {
icon: 'arc', // description: 'Start drawing an arc tangent to the current segment.',
displayName: 'Tangential Arc', // icon: 'arc',
}, // displayName: 'Tangential Arc',
'Equip rectangle tool': { // },
description: 'Start drawing a rectangle.', // 'Equip rectangle tool': {
icon: 'rectangle', // description: 'Start drawing a rectangle.',
displayName: 'Rectangle', // icon: 'rectangle',
}, // displayName: 'Rectangle',
// },
Export: { Export: {
description: 'Export the current model.', description: 'Export the current model.',
icon: 'exportFile', icon: 'exportFile',

View File

@ -10,7 +10,6 @@ import {
import { APP_VERSION } from 'routes/Settings' import { APP_VERSION } from 'routes/Settings'
import { UAParser } from 'ua-parser-js' import { UAParser } from 'ua-parser-js'
import screenshot from 'lib/screenshot' import screenshot from 'lib/screenshot'
import React from 'react'
import { VITE_KC_API_BASE_URL } from 'env' import { VITE_KC_API_BASE_URL } from 'env'
/* eslint-disable suggest-no-throw/suggest-no-throw -- /* eslint-disable suggest-no-throw/suggest-no-throw --
@ -33,17 +32,14 @@ import { VITE_KC_API_BASE_URL } from 'env'
// TODO: Throw more // TODO: Throw more
export class CoreDumpManager { export class CoreDumpManager {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
htmlRef: React.RefObject<HTMLDivElement> | null
token: string | undefined token: string | undefined
baseUrl: string = VITE_KC_API_BASE_URL baseUrl: string = VITE_KC_API_BASE_URL
constructor( constructor(
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
htmlRef: React.RefObject<HTMLDivElement> | null,
token: string | undefined token: string | undefined
) { ) {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
this.htmlRef = htmlRef
this.token = token this.token = token
} }
@ -73,31 +69,14 @@ export class CoreDumpManager {
// Get the os information. // Get the os information.
getOsInfo(): Promise<string> { getOsInfo(): Promise<string> {
if (this.isTauri()) { if (this.isTauri()) {
return tauriArch()
.catch((error: any) => {
throw new Error(`Error getting arch: ${error}`)
})
.then((arch: string) => {
return tauriPlatform()
.catch((error: any) => {
throw new Error(`Error getting platform: ${error}`)
})
.then((platform: string) => {
return tauriKernelVersion()
.catch((error: any) => {
throw new Error(`Error getting kernel version: ${error}`)
})
.then((kernelVersion: string) => {
const osinfo: OsInfo = { const osinfo: OsInfo = {
platform, platform: tauriPlatform(),
arch, arch: tauriArch(),
browser: 'tauri', browser: 'tauri',
version: kernelVersion, version: tauriKernelVersion(),
} }
return JSON.stringify(osinfo) return new Promise((resolve) => resolve(JSON.stringify(osinfo)))
}) // TODO: get rid of promises now that the tauri api doesn't require them anymore
})
})
} }
const userAgent = window.navigator.userAgent || 'unknown browser' const userAgent = window.navigator.userAgent || 'unknown browser'
@ -449,12 +428,11 @@ export class CoreDumpManager {
// Return a data URL (png format) of the screenshot of the current page. // Return a data URL (png format) of the screenshot of the current page.
screenshot(): Promise<string> { screenshot(): Promise<string> {
return screenshot(this.htmlRef) return (
.then((screenshot: string) => { screenshot()
return screenshot .then((screenshotStr: string) => screenshotStr)
}) // maybe rust should handle an error, but an empty string at least doesn't cause the core dump to fail entirely
.catch((error: any) => { .catch((error: any) => ``)
throw new Error(`Error getting screenshot: ${error}`) )
})
} }
} }

View File

@ -1,17 +1,15 @@
import React from 'react'
import html2canvas from 'html2canvas-pro' import html2canvas from 'html2canvas-pro'
// Return a data URL (png format) of the screenshot of the current page. // Return a data URL (png format) of the screenshot of the current page.
export default async function screenshot( export default async function screenshot(): Promise<string> {
htmlRef: React.RefObject<HTMLDivElement> | null if (typeof window === 'undefined') {
): Promise<string> { return Promise.reject(
if (htmlRef === null) { new Error(
return Promise.reject(new Error('htmlRef is null')) "element isn't defined because there's no window, are you running in Node?"
)
)
} }
if (htmlRef.current === null) { return html2canvas(document.documentElement)
return Promise.reject(new Error('htmlRef is null'))
}
return html2canvas(htmlRef.current)
.then((canvas) => { .then((canvas) => {
return canvas.toDataURL() return canvas.toDataURL()
}) })

View File

@ -8,8 +8,7 @@ import {
import { CallExpression, SourceRange, Value, parse, recast } from 'lang/wasm' import { CallExpression, SourceRange, Value, parse, recast } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EditorSelection } from '@codemirror/state' import { EditorSelection, SelectionRange } from '@codemirror/state'
import { SelectionRange } from '@uiw/react-codemirror'
import { getNormalisedCoordinates, isOverlap } from 'lib/utils' import { getNormalisedCoordinates, isOverlap } from 'lib/utils'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { Program } from 'lang/wasm' import { Program } from 'lang/wasm'

View File

@ -11,6 +11,8 @@ export const engineCommandManager = new EngineCommandManager()
// This needs to be after codeManager is created. // This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager) export const kclManager = new KclManager(engineCommandManager)
kclManager.isFirstRender = true
engineCommandManager.getAstCb = () => kclManager.ast engineCommandManager.getAstCb = () => kclManager.ast
export const sceneInfra = new SceneInfra(engineCommandManager) export const sceneInfra = new SceneInfra(engineCommandManager)

File diff suppressed because one or more lines are too long

View File

@ -39,7 +39,6 @@ import {
listProjects, listProjects,
renameProjectDirectory, renameProjectDirectory,
} from 'lib/tauri' } from 'lib/tauri'
import { useAppState } from 'AppState'
// This route only opens in the Tauri desktop context for now, // This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types. // as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -66,11 +65,6 @@ const Home = () => {
} }
) )
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const { setAppState } = useAppState()
useEffect(() => {
setAppState({ htmlRef: ref })
}, [ref])
const [state, send, actor] = useMachine(homeMachine, { const [state, send, actor] = useMachine(homeMachine, {
context: { context: {

View File

@ -80,8 +80,10 @@ export const onboardingRoutes = [
export function useDemoCode() { export function useDemoCode() {
useEffect(() => { useEffect(() => {
if (!editorManager.editorView) return if (!editorManager.editorView) return
setTimeout(() => codeManager.updateCodeStateEditor(bracket)) setTimeout(() => {
}, [editorManager.editorView, codeManager.updateCodeStateEditor]) codeManager.updateCodeStateEditor(bracket)
})
}, [editorManager.editorView])
} }
export function useNextClick(newStatus: string) { export function useNextClick(newStatus: string) {

146
yarn.lock
View File

@ -1124,7 +1124,7 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.24.7" version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
@ -1175,17 +1175,17 @@
"@codemirror/view" "^6.17.0" "@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0" "@lezer/common" "^1.0.0"
"@codemirror/autocomplete@^6.16.3": "@codemirror/autocomplete@^6.17.0":
version "6.16.3" version "6.17.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz#04d5a4e4e44ccae1ba525d47db53a5479bf46338" resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz#24ff5fc37fd91f6439df6f4ff9c8e910cde1b053"
integrity sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA== integrity sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==
dependencies: dependencies:
"@codemirror/language" "^6.0.0" "@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0" "@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0" "@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0" "@lezer/common" "^1.0.0"
"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0", "@codemirror/commands@^6.6.0": "@codemirror/commands@^6.0.0", "@codemirror/commands@^6.6.0":
version "6.6.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.6.0.tgz#d308f143fe1b8896ca25fdb855f66acdaf019dd4" resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.6.0.tgz#d308f143fe1b8896ca25fdb855f66acdaf019dd4"
integrity sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg== integrity sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==
@ -1234,12 +1234,12 @@
"@codemirror/view" "^6.0.0" "@codemirror/view" "^6.0.0"
crelt "^1.0.5" crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.2.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1": "@codemirror/state@^6.0.0", "@codemirror/state@^6.2.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1":
version "6.4.1" version "6.4.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b" resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b"
integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A== integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==
"@codemirror/theme-one-dark@^6.0.0": "@codemirror/theme-one-dark@^6.1.2":
version "6.1.2" version "6.1.2"
resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8" resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA== integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==
@ -1909,15 +1909,10 @@
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz#f519149bce9156d0e7954b9531df15f446f2fc12" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz#f519149bce9156d0e7954b9531df15f446f2fc12"
integrity sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ== integrity sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ==
"@tauri-apps/api@2.0.0-beta.12": "@tauri-apps/api@2.0.0-beta.14", "@tauri-apps/api@^2.0.0-beta.14":
version "2.0.0-beta.12" version "2.0.0-beta.14"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.12.tgz#0b552086e6382cfd5798537b304d00cbf42db7a1" resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.14.tgz#8c1c65c07559cd29c5103a99e0abe5331cc2246f"
integrity sha512-77OvAnsExtiprnjQcvmDyZGfnIvMF/zVL5+8Vkl1R8o8E3iDtvEJZpbbH1F4dPtNa3gr4byp/5dm8hAa1+r3AA== integrity sha512-YLYgHqdwWswr4Y70+hRzaLD6kLIUgHhE3shLXNquPiTaQ9+cX3Q2dB0AFfqsua6NXYFNe7LfkmMzaqEzqv3yQg==
"@tauri-apps/api@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.13.tgz#53ec5117d042d560615afec2d38a6d38ee20ff22"
integrity sha512-Np1opKANzRMF3lgJ9gDquBCB9SxlE2lRmNpVx1+L6RyzAmigkuh0ZulT5jMnDA3JLsuSDU135r/s4t/Pmx4atg==
"@tauri-apps/cli-darwin-arm64@2.0.0-beta.13": "@tauri-apps/cli-darwin-arm64@2.0.0-beta.13":
version "2.0.0-beta.13" version "2.0.0-beta.13"
@ -1985,54 +1980,54 @@
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-beta.13" "@tauri-apps/cli-win32-ia32-msvc" "2.0.0-beta.13"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-beta.13" "@tauri-apps/cli-win32-x64-msvc" "2.0.0-beta.13"
"@tauri-apps/plugin-dialog@^2.0.0-beta.2": "@tauri-apps/plugin-dialog@^2.0.0-beta.6":
version "2.0.0-beta.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-beta.5.tgz#1ad9592d554bd5a4b4d41a9f4aa6e01aa33a9f6a"
integrity sha512-jkaBCsx2v6WB6sB77fTMCeijuvT3FlzwschiHnPlD7aU6CHvQgRlpCv/FttPdTq4ih2t6MIlM4oX85hNYgfs6w==
dependencies:
"@tauri-apps/api" "2.0.0-beta.13"
"@tauri-apps/plugin-fs@^2.0.0-beta.5":
version "2.0.0-beta.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.5.tgz#eea9161d33dafc592005e3ee1e74db6298b20398"
integrity sha512-uTqCDFA1z8KDtTe5fKlbRrKV4zxh63UVUvD/PR8GnyNLV+qxj/fEuJuGvMdfMbVKrTljGqSpun5wnP5jqD5fMg==
dependencies:
"@tauri-apps/api" "2.0.0-beta.13"
"@tauri-apps/plugin-http@^2.0.0-beta.2":
version "2.0.0-beta.6" version "2.0.0-beta.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-http/-/plugin-http-2.0.0-beta.6.tgz#b126b6ca3ab22ccaeb9861fa4f5e88066e20539e" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-beta.6.tgz#e42b80278d914318992cfc0534bc3c6977ed52ac"
integrity sha512-/5cDY9LwrZkPBTqxx2xwvzA3fzYS+Y1UD0rK9NVxjKkNXoA9NmGxEetug05u0KPbOtciyFiTyq31koszlPy6KA== integrity sha512-Rw8C8t/0y3QExEinp+cAOZi/BDt0c9jifv0bS3WDCwQt9ANdmfCWKamsIhqwemt3MjepkU2RV8bvphzoWlbOGw==
dependencies: dependencies:
"@tauri-apps/api" "2.0.0-beta.13" "@tauri-apps/api" "2.0.0-beta.14"
"@tauri-apps/plugin-os@^2.0.0-beta.3": "@tauri-apps/plugin-fs@^2.0.0-beta.6":
version "2.0.0-beta.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.0.0-beta.5.tgz#7b3b066796e37f073925afb6cb0ddb9cdab0f7d9"
integrity sha512-Qfs/clZ9R05J+OVOGkko+9OaYaL+xJaGICeQ1G5CnLFpUdTfMV10D+1nBBauxDdiLU4ay5I0iprJ5aG5GJBunQ==
dependencies:
"@tauri-apps/api" "2.0.0-beta.13"
"@tauri-apps/plugin-process@^2.0.0-beta.2":
version "2.0.0-beta.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-process/-/plugin-process-2.0.0-beta.5.tgz#aa97b98ab4dcdd438de162e98f6f54e0230ff6e3"
integrity sha512-UMiBm6TtNYfxRb6GwzA4PalkZGwalHdclI/t0MVG33fNXgX1PaWONR/NZW/k7JE+WpvRAnN/Kf9ur8aEzjVVSQ==
dependencies:
"@tauri-apps/api" "2.0.0-beta.13"
"@tauri-apps/plugin-shell@^2.0.0-beta.2":
version "2.0.0-beta.6" version "2.0.0-beta.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.6.tgz#10378e753a220bdab638cbacb6b9d0039ddbe986" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.6.tgz#ac0e0b171e5c8320e26ca763e780d91a1d1e4e4a"
integrity sha512-g3nM9cQQGl7Iv4MvyFuco/aPTiwOI/MixcoKso3VQIg5Aqd64NqR0r+GfsB0qx52txItqzSXwmeaj1eZjO9Q6Q== integrity sha512-R7M5wggENzJhiA0HwP63AzdF6uzdXRYe0z+ETSue9gvJmqKtqVp+qx9JLsWSfwENHy8RDKmrnuDL18kx/Q2aFA==
dependencies: dependencies:
"@tauri-apps/api" "2.0.0-beta.13" "@tauri-apps/api" "2.0.0-beta.14"
"@tauri-apps/plugin-updater@^2.0.0-beta.3": "@tauri-apps/plugin-http@^2.0.0-beta.7":
version "2.0.0-beta.5" version "2.0.0-beta.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.5.tgz#8be3277da409ffc596a870b625cb4bb4ef17f199" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-http/-/plugin-http-2.0.0-beta.7.tgz#0472b6b71c9df5d889c8a81e136ce3dd824aeeb6"
integrity sha512-h8uNFQDtXaZPFyQcNAB5SxiSIvPbYRlM1fXf/lCcW8bAaMTanyszbHvR2vAYtVa/wIzKAOYriC/MpsJaKLjyhg== integrity sha512-mxdhcpPPT2oHm0FWe6HS2UbQW2LFTbAv2vExrTYAPJSuXOp2kisgOjazZtswYqpmftpcSZ4dFpmzNlQp188e/g==
dependencies: dependencies:
"@tauri-apps/api" "2.0.0-beta.13" "@tauri-apps/api" "2.0.0-beta.14"
"@tauri-apps/plugin-os@^2.0.0-beta.6":
version "2.0.0-beta.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.0.0-beta.6.tgz#2ce79d3edfa81401da4f4ff61549700f37766b61"
integrity sha512-28Ts286o4YH3vZ+swptVblRMuMa1MLjLbgPpnR1wuPNzzR4p7J6+Hr3Euge71RIsFJhjAeP1XkNbHgpAFj4Mpg==
dependencies:
"@tauri-apps/api" "2.0.0-beta.14"
"@tauri-apps/plugin-process@^2.0.0-beta.6":
version "2.0.0-beta.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-process/-/plugin-process-2.0.0-beta.6.tgz#3835d4d61a9565a8451bfc3e47afe827bba8f050"
integrity sha512-Rem3r8lGe6ZSvncqIV9xpq2hOey7krMoPh5nu7WxbR73LOSkRBUDaYMvZjXu1DrJ3LEyXxo48sp76+9MW2Rp/w==
dependencies:
"@tauri-apps/api" "2.0.0-beta.14"
"@tauri-apps/plugin-shell@^2.0.0-beta.7":
version "2.0.0-beta.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.7.tgz#43159959ff8ef83435df6d64be381606f6e02130"
integrity sha512-oJxWbEiNRcoMM0PrePjJnjPHEAN1sbYuWaQ1QMtLPdjHsl83RLk+RpFzkL5WvtGknfiKY7T2qEthOID4br+mvg==
dependencies:
"@tauri-apps/api" "2.0.0-beta.14"
"@tauri-apps/plugin-updater@^2.0.0-beta.6":
version "2.0.0-beta.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.6.tgz#2b889638ea65ec0cfafa8e8adc530e10dfd40278"
integrity sha512-CKgVNsbpGPp8yIUWitszdQ2DVglRZ8jescxacJ43HkaI7oz26EQPL3VwPjjwIQ1RX166JQcNJuM8grcZd8QjBg==
dependencies:
"@tauri-apps/api" "2.0.0-beta.14"
"@testing-library/dom@^10.0.0": "@testing-library/dom@^10.0.0":
version "10.1.0" version "10.1.0"
@ -2454,31 +2449,6 @@
"@typescript-eslint/types" "5.62.0" "@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@uiw/codemirror-extensions-basic-setup@4.22.2":
version "4.22.2"
resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.22.2.tgz#a114dc9ebad6de41a441c8aca655d9c34934a7d9"
integrity sha512-zcHGkldLFN3cGoI5XdOGAkeW24yaAgrDEYoyPyWHODmPiNwybQQoZGnH3qUdzZwUaXtAcLWoAeOPzfNRW2yGww==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/commands" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/search" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@uiw/react-codemirror@^4.21.25":
version "4.22.2"
resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.22.2.tgz#18dcb79e31cf34e0704366f3041da93ff3c64109"
integrity sha512-okCSl+WJG63gRx8Fdz7v0C6RakBQnbb3pHhuzIgDB+fwhipgFodSnu2n9oOsQesJ5YQ7mSOcKMgX0JEsu4nnfQ==
dependencies:
"@babel/runtime" "^7.18.6"
"@codemirror/commands" "^6.1.0"
"@codemirror/state" "^6.1.1"
"@codemirror/theme-one-dark" "^6.0.0"
"@uiw/codemirror-extensions-basic-setup" "4.22.2"
codemirror "^6.0.0"
"@ungap/structured-clone@^1.2.0": "@ungap/structured-clone@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@ -3530,7 +3500,7 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
codemirror@^6.0.0, codemirror@^6.0.1: codemirror@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==
@ -5145,10 +5115,10 @@ html-encoding-sniffer@^3.0.0:
dependencies: dependencies:
whatwg-encoding "^2.0.0" whatwg-encoding "^2.0.0"
html2canvas-pro@^1.5.1: html2canvas-pro@^1.5.2:
version "1.5.1" version "1.5.2"
resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.5.1.tgz#e62aea27c598152308be35754b8b0f188908e171" resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.5.2.tgz#fb9841feabd477d02e54ea60b4a45ef499f8350a"
integrity sha512-6LmbLb3qNg8f3jSSRdzDwG7c9VIFDC9jOP3iZ6mK1cjc9W6F2Mkh/n2WmBECxAzF2yHEznJFmgbQAut1g+NbEQ== integrity sha512-VYZifzRbLl+ssNDbivIAQftu+qRsxF3YdNpCo1NvqHAZ/0O3aoV0j1yIyIEKcDxTtuQ0buE3pe74IhmyRk/QdQ==
dependencies: dependencies:
css-line-break "^2.1.0" css-line-break "^2.1.0"
text-segmentation "^1.0.3" text-segmentation "^1.0.3"