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/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
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
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
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.
That will create the branch with the updated json files for you.
run `./make-release.sh` for a patch update
run `./make-release.sh "minor"` for minor
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
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
The PR may serve as a place to discuss the human-readable changelog and extra QA.
2. Merge the PR
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
3. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named)
## 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
// up, its an annoying codemirror thing.
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')
const foldGutterFoldLine = page.locator('[title="Fold line"]')
@ -746,12 +750,12 @@ test.describe('Editor tests', () => {
await page.keyboard.press('ArrowRight')
// 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
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase')
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
// 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.type('const topAng = 42')
await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight')
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 expect(page.locator('.cm-diagnosticText')).toBeVisible()
await expect(page.getByText('Cannot redefine `topAng`')).toBeVisible()
await expect(page.locator('.cm-diagnosticText').first()).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 page.keyboard.type('otherAng')
@ -929,7 +936,9 @@ test.describe('Editor tests', () => {
// error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
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
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 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()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(
page.getByText(
'sketch profile must lie entirely on one side of the revolution axis'
)
).toBeVisible()
const searchText =
'sketch profile must lie entirely on one side of the revolution axis'
await expect(page.getByText(searchText).first()).toBeVisible()
})
test.describe('Autocomplete works', () => {
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.
await page.waitForTimeout(800)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
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-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 }) => {
@ -2921,7 +2924,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
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.waitForTimeout(100)
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 expect(page.getByTestId('hover-highlight').first()).toBeVisible()
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
await page.mouse.click(pos[0], pos[1])
@ -3703,16 +3706,14 @@ test.describe('Regression tests', () => {
// Make sure it's not a link
await expect(zooLogo).not.toHaveAttribute('href')
})
test.fixme(
'Position _ Is Out Of Range... regression test',
async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const exampleSketch = startSketchOn("XZ")
test('Position _ Is Out Of Range... regression test', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %)
|> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %)
@ -3721,51 +3722,59 @@ test.describe('Regression tests', () => {
const example = extrude(5, exampleSketch)
shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)`
)
})
)
})
await u.waitForAuthSkipAppStart()
await page.goto('/')
await u.waitForPageLoad()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error in guter
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
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
// Okay execution finished, let's start editing text below the error.
await u.codeLocator.click()
// Go to the end of the editor
// This bug happens when there is a diagnostic in the editor and you try to
// edit text below it.
// Or delete a huge chunk of text and then try to edit below it.
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Okay execution finished, let's start editing text below the error.
await u.codeLocator.click()
// Go to the end of the editor
// This bug happens when there is a diagnostic in the editor and you try to
// edit text below it.
// Or delete a huge chunk of text and then try to edit below it.
await page.keyboard.press('End')
await page.keyboard.down('Shift')
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.press('Backspace')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await page.keyboard.type('thing: "blah"', { delay: 100 })
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await page.keyboard.type('thing: "blah"', { delay: 100 })
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowLeft')
await expect(page.locator('.cm-content'))
.toHaveText(`const exampleSketch = startSketchOn("XZ")
await expect(page.locator('.cm-content'))
.toContainText(`const exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %)
|> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %)
|> close(%)
thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
}
)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
})
})
test.describe('Sketch tests', () => {
@ -3856,6 +3865,31 @@ test.describe('Sketch tests', () => {
page.getByRole('button', { name: 'Edit Sketch' })
).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', () => {
const doEditSegmentsByDraggingHandle = async (
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 {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page),
removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => {

View File

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

View File

@ -1,10 +1,7 @@
import { autocompletion } from '@codemirror/autocomplete'
import { foldService, syntaxTree } from '@codemirror/language'
import { foldService } from '@codemirror/language'
import { Extension, EditorState } from '@codemirror/state'
import { ViewPlugin } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import {
docPathFacet,
LanguageServerPlugin,
@ -13,7 +10,6 @@ import {
workspaceFolders,
LanguageServerOptions,
} from './plugin/lsp'
import { offsetToPos } from './plugin/util'
export type { LanguageServerClientOptions } from './client'
export { LanguageServerClient } from './client'
@ -24,14 +20,15 @@ export {
LspWorkerEventType,
} from './client/codec'
export type { LanguageServerOptions } from './plugin/lsp'
export type { TransactionInfo, RelevantUpdate } from './plugin/annotations'
export { updateInfo, TransactionAnnotation } from './plugin/annotations'
export {
LanguageServerPlugin,
LanguageServerPluginSpec,
docPathFacet,
languageId,
workspaceFolders,
lspSemanticTokensEvent,
lspDiagnosticsEvent,
lspFormatCodeEvent,
} from './plugin/lsp'
export { posToOffset, offsetToPos } from './plugin/util'
@ -51,63 +48,10 @@ export function lspPlugin(options: LanguageServerOptions): Extension {
if (plugin == null) return null
// Get the folding ranges from the language server.
// 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
}

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 {
acceptCompletion,
autocompletion,
clearSnippet,
closeCompletion,
hasNextSnippetField,
@ -8,10 +9,17 @@ import {
prevSnippetField,
startCompletion,
} from '@codemirror/autocomplete'
import { Prec } from '@codemirror/state'
import { EditorView, keymap, KeyBinding } from '@codemirror/view'
import { Prec, Extension } from '@codemirror/state'
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(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
@ -46,6 +54,59 @@ const lspAutocompleteKeymap: readonly KeyBinding[] = [
},
]
export const lspAutocompleteKeymapExt = Prec.highest(
keymap.computeN([], () => [lspAutocompleteKeymap])
)
const lspAutocompleteKeymapExt = Prec.highest(keymap.of(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,
} 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 {
ViewUpdate,
PluginValue,
@ -22,15 +28,10 @@ import {
import { URI } from 'vscode-uri'
import { LanguageServerClient } from '../client'
import {
lspSemanticTokensEvent,
lspFormatCodeEvent,
relevantUpdate,
} from './annotations'
import { CompletionItemKindMap } from './autocomplete'
import { addToken, SemanticToken } from './semantic-tokens'
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
import { lspAutocompleteKeymapExt } from './autocomplete'
import lspAutocompleteExt from './autocomplete'
import lspHoverExt from './hover'
import lspFormatExt from './format'
import lspIndentExt from './indent'
@ -47,6 +48,17 @@ export const workspaceFolders = Facet.define<
LSP.WorkspaceFolder[]
>({ 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 {
// We assume this is the main project directory, we are currently working in.
workspaceFolders: LSP.WorkspaceFolder[]
@ -131,11 +143,6 @@ export class LanguageServerPlugin implements PluginValue {
}
update(viewUpdate: ViewUpdate) {
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
}
// If the doc didn't change we can return early.
if (!viewUpdate.docChanged) {
return
@ -284,19 +291,16 @@ 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({
changes: {
from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!,
insert: newText,
},
annotations: [lspFormatCodeEvent, Transaction.addToHistory.of(true)],
})
}
this.view.dispatch({
changes: result.map(({ range, newText }) => ({
from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!,
insert: newText,
})),
annotations: lspFormatCodeEvent,
})
}
async requestCompletion(
@ -552,7 +556,7 @@ export class LanguageServerPluginSpec
{
provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension {
return [
lspAutocompleteKeymapExt,
lspAutocompleteExt(plugin),
lspFormatExt(plugin),
lspHoverExt(plugin),
lspIndentExt(),

View File

@ -4,7 +4,7 @@ import { EditorView, Decoration, DecorationSet } from '@codemirror/view'
import { Tag, tags } from '@lezer/highlight'
import { lspSemanticTokensEvent } from './annotations'
import { lspSemanticTokensEvent } from './lsp'
export interface SemanticToken {
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
[build-dependencies]
tauri-build = { version = "2.0.0-beta.13", features = [] }
tauri-build = { version = "2.0.0-beta.18", features = [] }
[dependencies]
anyhow = "1"
@ -20,18 +20,18 @@ kittycad = "0.3.5"
log = "0.4.21"
oauth2 = "4.4.2"
serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
tauri = { version = "2.0.0-beta.23", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.7" }
tauri-plugin-deep-link = { version = "2.0.0-beta.8" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
tauri-plugin-fs = { version = "2.0.0-beta.9" }
tauri-plugin-http = { version = "2.0.0-beta.6" }
tauri-plugin-log = { version = "2.0.0-beta.4" }
tauri-plugin-os = { version = "2.0.0-beta.2" }
tauri-plugin-persisted-scope = { version = "2.0.0-beta.7" }
tauri-plugin-process = { version = "2.0.0-beta.2" }
tauri-plugin-shell = { version = "2.0.0-beta.2" }
tauri-plugin-updater = { version = "2.0.0-beta.4" }
tauri-plugin-fs = { version = "2.0.0-beta.10" }
tauri-plugin-http = { version = "2.0.0-beta.11" }
tauri-plugin-log = { version = "2.0.0-beta.7" }
tauri-plugin-os = { version = "2.0.0-beta.7" }
tauri-plugin-persisted-scope = { version = "2.0.0-beta.10" }
tauri-plugin-process = { version = "2.0.0-beta.7" }
tauri-plugin-shell = { version = "2.0.0-beta.8" }
tauri-plugin-updater = { version = "2.0.0-beta.9" }
tokio = { version = "1.37.0", features = ["time", "fs", "process"] }
toml = "0.8.2"
url = "2.5.0"

View File

@ -63,16 +63,22 @@
"subcommands": {}
},
"deep-link": {
"domains": [
"mobile": [
{
"host": "app.zoo.dev"
}
]
],
"desktop": {
"schemes": [
"zoo",
"zoo-modeling-app"
]
}
},
"shell": {
"open": true
}
},
"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 Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump'
import { useAppState } from 'AppState'
export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS')
@ -45,17 +44,12 @@ export function App() {
useHotKeyListener()
const { context } = useModelingContext()
const { setAppState } = useAppState()
useEffect(() => {
setAppState({ htmlRef: ref })
}, [ref])
const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token
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 {
isStreamReady: boolean
htmlRef: React.RefObject<HTMLDivElement> | null
setAppState: (newAppState: Partial<AppState>) => void
}
const AppStateContext = createContext<AppState>({
htmlRef: null,
isStreamReady: false,
setAppState: () => {},
})
@ -24,7 +22,6 @@ export const useAppState = () => useContext(AppStateContext)
export const AppStateProvider = ({ children }: { children: ReactNode }) => {
const [appState, _setAppState] = useState<AppState>({
htmlRef: null,
isStreamReady: false,
setAppState: () => {},
})
@ -34,7 +31,6 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
return (
<AppStateContext.Provider
value={{
htmlRef: appState.htmlRef,
isStreamReady: appState.isStreamReady,
setAppState,
}}

View File

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

View File

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

View File

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

View File

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

View File

@ -64,7 +64,7 @@ import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
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 { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
@ -128,7 +128,7 @@ export const ModelingMachineProvider = ({
'enable copilot': () => {
editorManager.setCopilotEnabled(true)
},
'sketch exit execute': () => {
'sketch exit execute': ({ store }) => {
;(async () => {
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({
@ -438,17 +441,6 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length <= 0) return false
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 }) => {
if (data?.forceNewSketch) return false
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 { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
@ -43,6 +42,7 @@ import {
closeBracketsKeymap,
completionKeymap,
} from '@codemirror/autocomplete'
import CodeEditor from './CodeEditor'
export const editorShortcutMeta = {
formatCode: {
@ -185,15 +185,15 @@ export const KclEditorPane = () => {
id="code-mirror-override"
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
>
<ReactCodeMirror
value={initialCode.current}
<CodeEditor
initialDocValue={initialCode.current}
extensions={editorExtensions}
theme={theme}
onCreateEditor={(_editorView) => {
if (_editorView === null) return
editorManager.setEditorView(_editorView)
}}
indentWithTab={false}
basicSetup={false}
/>
</div>
)

View File

@ -6,14 +6,12 @@ import React, { useMemo } from 'react'
import toast from 'react-hot-toast'
import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAppState } from 'AppState'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const { htmlRef } = useAppState()
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`,
})
},
'Execute AST': () => kclManager.executeCode(true, true),
'Execute AST': () => {
kclManager.isFirstRender = true
kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
},
services: {
'Persist settings': (context) =>

View File

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

View File

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

View File

@ -23,17 +23,12 @@ import {
} from '@codemirror/state'
import { completionStatus } from '@codemirror/autocomplete'
import {
TransactionAnnotation,
offsetToPos,
posToOffset,
LanguageServerOptions,
LanguageServerClient,
docPathFacet,
languageId,
TransactionInfo,
updateInfo,
RelevantUpdate,
lspPlugin,
} from '@kittycad/codemirror-lsp-client'
import { deferExecution } from 'lib/utils'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
@ -98,11 +93,6 @@ const completionDecoration = StateField.define<CompletionState>({
return state
}
// We only care about transactions with effects.
if (!transaction.effects) {
return state
}
for (const effect of transaction.effects) {
if (effect.is(addSuggestion)) {
// 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
export class CompletionRequester implements PluginValue {
private client: LanguageServerClient
private lastPos: number = 0
private viewUpdate: ViewUpdate | null = null
private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.requestCompletions()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.rejectSuggestionCommand()
}, changesDelay)
constructor(client: LanguageServerClient) {
constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client
}
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.
if (!editorManager.copilotEnabled) {
return
}
this.lastPos = this.viewUpdate.state.selection.main.head
this._deffererCodeUpdate(true)
let isUserSelect = false
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 {
if (!this.viewUpdate) {
return null
}
return (
this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null
)
return this.view.state.field(completionDecoration)?.ghostText || null
}
containsGhostText(): boolean {
@ -294,33 +257,23 @@ export class CompletionRequester implements PluginValue {
}
autocompleting(): boolean {
if (!this.viewUpdate) {
return false
}
return completionStatus(this.viewUpdate.state) === 'active'
return completionStatus(this.view.state) === 'active'
}
notFocused(): boolean {
if (!this.viewUpdate) {
return true
}
return !this.viewUpdate.view.hasFocus
return !this.view.hasFocus
}
async requestCompletions(): Promise<void> {
if (
this.viewUpdate === null ||
this.containsGhostText() ||
this.autocompleting() ||
this.notFocused() ||
!this.viewUpdate.docChanged
this.notFocused()
) {
return
}
const pos = this.viewUpdate.state.selection.main.head
const pos = this.view.state.selection.main.head
// Check if the position has changed
if (pos !== this.lastPos) {
@ -328,7 +281,7 @@ export class CompletionRequester implements PluginValue {
}
// Get the current position and source
const state = this.viewUpdate.state
const state = this.view.state
const dUri = state.facet(docPathFacet)
// Request completion from the server
@ -396,14 +349,14 @@ export class CompletionRequester implements PluginValue {
// 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.
const line = this.viewUpdate.view.state.doc.lineAt(pos)
const line = this.view.state.doc.lineAt(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)) {
displayText = displayText.slice(0, displayText.length - ending.length)
} else if (displayText.includes(ending)) {
// Remove the ending
this.viewUpdate.view.dispatch({
this.view.dispatch({
changes: {
from: pos,
to: line.to,
@ -416,7 +369,7 @@ export class CompletionRequester implements PluginValue {
}
}
this.viewUpdate.view.dispatch({
this.view.dispatch({
changes: {
from: pos,
to: pos,
@ -442,10 +395,6 @@ export class CompletionRequester implements PluginValue {
}
acceptSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText()
if (!ghostText) {
return false
@ -463,7 +412,7 @@ export class CompletionRequester implements PluginValue {
const suggestion = ghostText.text
this.viewUpdate.view.dispatch({
this.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
@ -475,7 +424,7 @@ export class CompletionRequester implements PluginValue {
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
this.viewUpdate.view.dispatch({
this.view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
@ -490,10 +439,6 @@ export class CompletionRequester implements PluginValue {
}
rejectSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText()
if (!ghostText) {
return false
@ -503,7 +448,7 @@ export class CompletionRequester implements PluginValue {
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
this.viewUpdate.view.dispatch({
this.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
@ -521,10 +466,6 @@ export class CompletionRequester implements PluginValue {
}
sameKeyCommand(key: string) {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText()
if (!ghostText) {
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
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)) {
this.viewUpdate.view.dispatch({
this.view.dispatch({
selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
@ -551,7 +492,7 @@ export class CompletionRequester implements PluginValue {
return this.acceptSuggestionCommand()
} else {
// Use this to delete the first letter of the suggestion
this.viewUpdate.view.dispatch({
this.view.dispatch({
selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
@ -598,7 +539,7 @@ export class CompletionRequester implements PluginValue {
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: CompletionRequester | null = null
const completionPlugin = ViewPlugin.define(
(view) => (plugin = new CompletionRequester(options.client))
(view) => (plugin = new CompletionRequester(view, options.client))
)
const domHandlers = EditorView.domEventHandlers({
@ -625,8 +566,6 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
})
const rejectSuggestionCommand = (view: EditorView): boolean => {
if (view.plugin === null) return false
// Get the current plugin from the map.
const p = view.plugin(completionPlugin)
if (p === null) return false
@ -681,7 +620,6 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
)
return [
lspPlugin(options),
completionPlugin,
copilotAutocompleteKeymapExt,
domHandlers,

View File

@ -2,12 +2,9 @@ import { Extension } from '@codemirror/state'
import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view'
import {
LanguageServerOptions,
updateInfo,
TransactionInfo,
RelevantUpdate,
TransactionAnnotation,
LanguageServerClient,
lspPlugin,
lspFormatCodeEvent,
} from '@kittycad/codemirror-lsp-client'
import { deferExecution } from 'lib/utils'
import { codeManager, editorManager, kclManager } from 'lib/singletons'
@ -18,34 +15,6 @@ import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecute
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
export class KclPlugin implements PluginValue {
private viewUpdate: ViewUpdate | null = null
@ -75,18 +44,38 @@ export class KclPlugin implements PluginValue {
this.viewUpdate = viewUpdate
editorManager.setEditorView(viewUpdate.view)
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
let isUserSelect = false
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(lspFormatCodeEvent.type)) {
isRelevant = true
}
}
// If we have a user select event, we want to update what parts are
// highlighted.
if (isRelevant.userSelect) {
if (isUserSelect) {
this._deffererUserSelect(true)
return
}
if (!isRelevant) {
return
}
if (!viewUpdate.docChanged) {
return
}

View File

@ -46,15 +46,7 @@ class KclLanguage extends Language {
const parser = new KclParser()
super(
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'
)
super(data, parser, [plugin], 'kcl')
}
}

View File

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

View File

@ -260,3 +260,8 @@ code {
@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,
recast,
SketchGroup,
SourceRange,
ExtrudeGroup,
} from 'lang/wasm'
import { getNodeFromPath } from './queryAst'
@ -65,6 +66,8 @@ export class KclManager {
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {}
isFirstRender = true
get ast() {
return this._ast
}
@ -194,7 +197,11 @@ export class KclManager {
async executeAst(
ast: Program = this._ast,
zoomToFit?: boolean,
executionId?: number
executionId?: number,
zoomOnRangeAndType?: {
range: SourceRange
type: string
}
): Promise<void> {
await this?.engineCommandManager?.waitForReady
const currentExecutionId = executionId || Date.now()
@ -218,12 +225,20 @@ export class KclManager {
defaultSelectionFilter(programMemory, this.engineCommandManager)
if (zoomToFit) {
let zoomObjectId: string | undefined = ''
if (zoomOnRangeAndType) {
zoomObjectId = this.engineCommandManager?.mapRangeToObjectId(
zoomOnRangeAndType.range,
zoomOnRangeAndType.type
)
}
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
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
},
})
@ -357,6 +372,11 @@ export class KclManager {
execute: boolean,
optionalParams?: {
focusPath?: PathToNode
zoomToFit?: boolean
zoomOnRangeAndType?: {
range: SourceRange
type: string
}
}
): Promise<{
newAst: Program
@ -400,7 +420,12 @@ export class KclManager {
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
await this.executeAst(astWithUpdatedSource)
await this.executeAst(
astWithUpdatedSource,
optionalParams?.zoomToFit,
undefined,
optionalParams?.zoomOnRangeAndType
)
} else {
// 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

View File

@ -6,7 +6,8 @@ import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast'
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'

View File

@ -2084,4 +2084,25 @@ export class EngineCommandManager extends EventTarget {
setScaleGridVisibility(visible: boolean) {
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.',
icon: 'sketch',
},
'Equip Line tool': {
description: 'Start drawing straight lines.',
icon: 'line',
displayName: 'Line',
},
'Equip tangential arc to': {
description: 'Start drawing an arc tangent to the current segment.',
icon: 'arc',
displayName: 'Tangential Arc',
},
'Equip rectangle tool': {
description: 'Start drawing a rectangle.',
icon: 'rectangle',
displayName: 'Rectangle',
},
// TODO the event is no 'change tool' with data: 'line', 'rectangle' etc
// 'Equip Line tool': {
// description: 'Start drawing straight lines.',
// icon: 'line',
// displayName: 'Line',
// },
// 'Equip tangential arc to': {
// description: 'Start drawing an arc tangent to the current segment.',
// icon: 'arc',
// displayName: 'Tangential Arc',
// },
// 'Equip rectangle tool': {
// description: 'Start drawing a rectangle.',
// icon: 'rectangle',
// displayName: 'Rectangle',
// },
Export: {
description: 'Export the current model.',
icon: 'exportFile',

View File

@ -10,7 +10,6 @@ import {
import { APP_VERSION } from 'routes/Settings'
import { UAParser } from 'ua-parser-js'
import screenshot from 'lib/screenshot'
import React from 'react'
import { VITE_KC_API_BASE_URL } from 'env'
/* eslint-disable suggest-no-throw/suggest-no-throw --
@ -33,17 +32,14 @@ import { VITE_KC_API_BASE_URL } from 'env'
// TODO: Throw more
export class CoreDumpManager {
engineCommandManager: EngineCommandManager
htmlRef: React.RefObject<HTMLDivElement> | null
token: string | undefined
baseUrl: string = VITE_KC_API_BASE_URL
constructor(
engineCommandManager: EngineCommandManager,
htmlRef: React.RefObject<HTMLDivElement> | null,
token: string | undefined
) {
this.engineCommandManager = engineCommandManager
this.htmlRef = htmlRef
this.token = token
}
@ -73,31 +69,14 @@ export class CoreDumpManager {
// Get the os information.
getOsInfo(): Promise<string> {
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 = {
platform,
arch,
browser: 'tauri',
version: kernelVersion,
}
return JSON.stringify(osinfo)
})
})
})
const osinfo: OsInfo = {
platform: tauriPlatform(),
arch: tauriArch(),
browser: 'tauri',
version: tauriKernelVersion(),
}
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'
@ -449,12 +428,11 @@ export class CoreDumpManager {
// Return a data URL (png format) of the screenshot of the current page.
screenshot(): Promise<string> {
return screenshot(this.htmlRef)
.then((screenshot: string) => {
return screenshot
})
.catch((error: any) => {
throw new Error(`Error getting screenshot: ${error}`)
})
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) => ``)
)
}
}

View File

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

View File

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

View File

@ -11,6 +11,8 @@ export const engineCommandManager = new EngineCommandManager()
// This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager)
kclManager.isFirstRender = true
engineCommandManager.getAstCb = () => kclManager.ast
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,
renameProjectDirectory,
} from 'lib/tauri'
import { useAppState } from 'AppState'
// 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.
@ -66,11 +65,6 @@ const Home = () => {
}
)
const ref = useRef<HTMLDivElement>(null)
const { setAppState } = useAppState()
useEffect(() => {
setAppState({ htmlRef: ref })
}, [ref])
const [state, send, actor] = useMachine(homeMachine, {
context: {

View File

@ -80,8 +80,10 @@ export const onboardingRoutes = [
export function useDemoCode() {
useEffect(() => {
if (!editorManager.editorView) return
setTimeout(() => codeManager.updateCodeStateEditor(bracket))
}, [editorManager.editorView, codeManager.updateCodeStateEditor])
setTimeout(() => {
codeManager.updateCodeStateEditor(bracket)
})
}, [editorManager.editorView])
}
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"
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"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
@ -1175,17 +1175,17 @@
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/autocomplete@^6.16.3":
version "6.16.3"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz#04d5a4e4e44ccae1ba525d47db53a5479bf46338"
integrity sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA==
"@codemirror/autocomplete@^6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz#24ff5fc37fd91f6439df6f4ff9c8e910cde1b053"
integrity sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.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"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.6.0.tgz#d308f143fe1b8896ca25fdb855f66acdaf019dd4"
integrity sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==
@ -1234,12 +1234,12 @@
"@codemirror/view" "^6.0.0"
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"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b"
integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==
"@codemirror/theme-one-dark@^6.0.0":
"@codemirror/theme-one-dark@^6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
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"
integrity sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ==
"@tauri-apps/api@2.0.0-beta.12":
version "2.0.0-beta.12"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.12.tgz#0b552086e6382cfd5798537b304d00cbf42db7a1"
integrity sha512-77OvAnsExtiprnjQcvmDyZGfnIvMF/zVL5+8Vkl1R8o8E3iDtvEJZpbbH1F4dPtNa3gr4byp/5dm8hAa1+r3AA==
"@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/api@2.0.0-beta.14", "@tauri-apps/api@^2.0.0-beta.14":
version "2.0.0-beta.14"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.14.tgz#8c1c65c07559cd29c5103a99e0abe5331cc2246f"
integrity sha512-YLYgHqdwWswr4Y70+hRzaLD6kLIUgHhE3shLXNquPiTaQ9+cX3Q2dB0AFfqsua6NXYFNe7LfkmMzaqEzqv3yQg==
"@tauri-apps/cli-darwin-arm64@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-x64-msvc" "2.0.0-beta.13"
"@tauri-apps/plugin-dialog@^2.0.0-beta.2":
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":
"@tauri-apps/plugin-dialog@^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"
integrity sha512-/5cDY9LwrZkPBTqxx2xwvzA3fzYS+Y1UD0rK9NVxjKkNXoA9NmGxEetug05u0KPbOtciyFiTyq31koszlPy6KA==
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-beta.6.tgz#e42b80278d914318992cfc0534bc3c6977ed52ac"
integrity sha512-Rw8C8t/0y3QExEinp+cAOZi/BDt0c9jifv0bS3WDCwQt9ANdmfCWKamsIhqwemt3MjepkU2RV8bvphzoWlbOGw==
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":
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":
"@tauri-apps/plugin-fs@^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"
integrity sha512-g3nM9cQQGl7Iv4MvyFuco/aPTiwOI/MixcoKso3VQIg5Aqd64NqR0r+GfsB0qx52txItqzSXwmeaj1eZjO9Q6Q==
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.6.tgz#ac0e0b171e5c8320e26ca763e780d91a1d1e4e4a"
integrity sha512-R7M5wggENzJhiA0HwP63AzdF6uzdXRYe0z+ETSue9gvJmqKtqVp+qx9JLsWSfwENHy8RDKmrnuDL18kx/Q2aFA==
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":
version "2.0.0-beta.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.5.tgz#8be3277da409ffc596a870b625cb4bb4ef17f199"
integrity sha512-h8uNFQDtXaZPFyQcNAB5SxiSIvPbYRlM1fXf/lCcW8bAaMTanyszbHvR2vAYtVa/wIzKAOYriC/MpsJaKLjyhg==
"@tauri-apps/plugin-http@^2.0.0-beta.7":
version "2.0.0-beta.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-http/-/plugin-http-2.0.0-beta.7.tgz#0472b6b71c9df5d889c8a81e136ce3dd824aeeb6"
integrity sha512-mxdhcpPPT2oHm0FWe6HS2UbQW2LFTbAv2vExrTYAPJSuXOp2kisgOjazZtswYqpmftpcSZ4dFpmzNlQp188e/g==
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":
version "10.1.0"
@ -2454,31 +2449,6 @@
"@typescript-eslint/types" "5.62.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":
version "1.2.0"
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"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
codemirror@^6.0.0, codemirror@^6.0.1:
codemirror@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==
@ -5145,10 +5115,10 @@ html-encoding-sniffer@^3.0.0:
dependencies:
whatwg-encoding "^2.0.0"
html2canvas-pro@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.5.1.tgz#e62aea27c598152308be35754b8b0f188908e171"
integrity sha512-6LmbLb3qNg8f3jSSRdzDwG7c9VIFDC9jOP3iZ6mK1cjc9W6F2Mkh/n2WmBECxAzF2yHEznJFmgbQAut1g+NbEQ==
html2canvas-pro@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.5.2.tgz#fb9841feabd477d02e54ea60b4a45ef499f8350a"
integrity sha512-VYZifzRbLl+ssNDbivIAQftu+qRsxF3YdNpCo1NvqHAZ/0O3aoV0j1yIyIEKcDxTtuQ0buE3pe74IhmyRk/QdQ==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"