Compare commits

..

1 Commits

Author SHA1 Message Date
f46edcddf3 remove the copies everywhere
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-29 16:31:01 -07:00
159 changed files with 5099 additions and 7451 deletions

View File

@ -138,7 +138,6 @@ 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'
@ -239,8 +238,12 @@ jobs:
shell: cmd shell: cmd
- name: Build the app (debug) - name: Build the app (debug)
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'false' }} if: ${{ env.BUILD_RELEASE == 'false' }}
run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}" with:
includeRelease: false
includeDebug: true
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- name: Build for Mac TestFlight (nightly) - name: Build for Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }} if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
@ -333,6 +336,7 @@ jobs:
# specific and we want to overwrite it with the this new build after and # specific and we want to overwrite it with the this new build after and
# not upload the apple store build to the public bucket # not upload the apple store build to the public bucket
- name: Build the app (release) and sign - name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' }}
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
@ -344,7 +348,8 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}" TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}" with:
args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest' if: matrix.os != 'ubuntu-latest'
@ -362,7 +367,7 @@ jobs:
export VITE_KC_API_BASE_URL export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri xvfb-run yarn test:e2e:tauri
env: env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app" E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/app"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }} KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run e2e tests (windows only) - name: Run e2e tests (windows only)
@ -371,15 +376,13 @@ jobs:
cargo install tauri-driver --force cargo install tauri-driver --force
yarn wdio run wdio.conf.ts yarn wdio run wdio.conf.ts
env: env:
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe" E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\app.exe"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }} KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }} VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
E2E_TAURI_ENABLED: true E2E_TAURI_ENABLED: true
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}' TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
publish-apps-release: publish-apps-release:
permissions:
contents: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps] needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]

2
.gitignore vendored
View File

@ -56,5 +56,3 @@ src-tauri/gen
src/wasm-lib/grackle/stdlib_cube_partial.json src/wasm-lib/grackle/stdlib_cube_partial.json
Mac_App_Distribution.provisionprofile Mac_App_Distribution.provisionprofile
*.tsbuildinfo

View File

@ -1,6 +1,5 @@
# Ignore artifacts: # Ignore artifacts:
build build
dist
coverage coverage
# Ignore Rust projects: # Ignore Rust projects:
@ -10,6 +9,5 @@ src/wasm-lib/pkg
src/wasm-lib/kcl/bindings src/wasm-lib/kcl/bindings
e2e/playwright/export-snapshots e2e/playwright/export-snapshots
# XState generated files # XState generated files
src/machines/**.typegen.ts src/machines/**.typegen.ts

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"cSpell.words": [
"geos"
],
"editor.tabSize": 2,
"editor.insertSpaces": true,
}

View File

@ -124,20 +124,36 @@ Before you submit a contribution PR to this repo, please ensure that:
## Release a new version ## Release a new version
1. Bump the versions by running `./make-realease.sh` while on a fresh pull of main 1. Bump the versions in the .json files by creating a `Cut release v{x}.{y}.{z}` PR, committing the changes from
That will create the branch with the updated json files for you. ```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.
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
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) 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
The PR may serve as a place to discuss the human-readable changelog and extra QA. ```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
2. Merge the PR 2. Merge the PR
3. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named) 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
## Fuzzing the parser ## Fuzzing the parser

File diff suppressed because it is too large Load Diff

View File

@ -91,9 +91,8 @@ const part001 = startSketchOn('-XZ')
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude') await u.waitForCmdReceive('extrude')
@ -331,7 +330,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -387,8 +386,8 @@ test('Draft segments should look right', async ({ page, context }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await expect( await expect(
@ -444,7 +443,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -491,7 +490,7 @@ test.describe('Client side scene scale should match engine scale', () => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -590,7 +589,7 @@ test.describe('Client side scene scale should match engine scale', () => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -690,7 +689,7 @@ const part002 = startSketchOn(part001, 'seg01')
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -740,7 +739,7 @@ test('Zoom to fit on load - solid 2d', async ({ page, context }) => {
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -777,7 +776,7 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -796,83 +795,3 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
}) })
test.describe('Grid visibility', () => {
test('Grid turned off', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(2)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test('Grid turned on', async ({ page }) => {
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: {
...TEST_SETTINGS,
modeling: {
...TEST_SETTINGS.modeling,
showScaleGrid: true,
},
},
}),
}
)
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(2)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 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: 33 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: 70 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -207,23 +207,6 @@ export const getMovementUtils = (opts: any) => {
return { toSU, click00r } return { toSU, click00r }
} }
async function waitForAuthAndLsp(page: Page) {
const waitForLspPromise = page.waitForEvent('console', async (message) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) {
await new Promise((resolve) => setTimeout(resolve, 200))
return true
}
return false
})
await page.goto('/')
await waitForPageLoad(page)
return waitForLspPromise
}
export async function getUtils(page: Page) { export async function getUtils(page: Page) {
// Chrome devtools protocol session only works in Chromium // Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name() const browserType = page.context().browser()?.browserType().name()
@ -231,8 +214,7 @@ export async function getUtils(page: Page) {
browserType !== 'chromium' ? null : await page.context().newCDPSession(page) browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
return { return {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page), waitForAuthSkipAppStart: () => waitForPageLoad(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]) => {

3
examples/addition.cado Normal file
View File

@ -0,0 +1,3 @@
// comment
const hi = 5 + 4

View File

@ -1,43 +1,50 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.23.1", "version": "0.22.6",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.17.0", "@codemirror/autocomplete": "^6.16.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/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.69", "@kittycad/lib": "^0.0.67",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@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.14", "@tauri-apps/api": "2.0.0-beta.12",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.6", "@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.6", "@tauri-apps/plugin-fs": "^2.0.0-beta.3",
"@tauri-apps/plugin-http": "^2.0.0-beta.7", "@tauri-apps/plugin-http": "^2.0.0-beta.2",
"@tauri-apps/plugin-os": "^2.0.0-beta.6", "@tauri-apps/plugin-os": "^2.0.0-beta.3",
"@tauri-apps/plugin-process": "^2.0.0-beta.6", "@tauri-apps/plugin-process": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.7", "@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@tauri-apps/plugin-updater": "^2.0.0-beta.6", "@tauri-apps/plugin-updater": "^2.0.0-beta.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@testing-library/user-event": "^14.5.2",
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@tweenjs/tween.js": "^23.1.1",
"@types/node": "^18.19.31",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.2.25",
"@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", "crypto-js": "^4.2.0",
"debounce-promise": "^3.1.2",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"formik": "^2.4.6",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.2", "html2canvas-pro": "^1.4.3",
"http-server": "^14.1.1",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"node-fetch": "^3.3.2",
"re-resizable": "^6.9.11", "re-resizable": "^6.9.11",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -48,15 +55,20 @@
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"three": "^0.166.1", "swr": "^2.2.5",
"three": "^0.164.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vitest": "^1.6.0",
"vscode-jsonrpc": "^8.2.1", "vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8", "wasm-pack": "^0.12.1",
"web-vitals": "^3.5.2", "web-vitals": "^3.5.2",
"xstate": "^4.38.2" "ws": "^8.17.0",
"xstate": "^4.38.2",
"zustand": "^4.5.2"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -73,8 +85,8 @@
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts", "test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
@ -109,16 +121,13 @@
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.24.3", "@babel/preset-env": "^7.24.3",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@playwright/test": "^1.45.1", "@playwright/test": "^1.44.1",
"@tauri-apps/cli": "==2.0.0-beta.13", "@tauri-apps/cli": "^2.0.0-beta.13",
"@testing-library/jest-dom": "^5.14.1", "@types/crypto-js": "^4.2.2",
"@testing-library/react": "^15.0.2", "@types/debounce-promise": "^3.1.9",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^18.19.31",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.2.25",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
@ -138,27 +147,21 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10", "happy-dom": "^14.3.10",
"http-server": "^14.1.1",
"husky": "^9.0.11", "husky": "^9.0.11",
"node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8", "prettier": "^2.8.0",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"vite": "^5.2.9", "vite": "^5.2.9",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0", "wait-on": "^7.2.0",
"wasm-pack": "^0.13.0",
"ws": "^8.17.0",
"yarn": "^1.22.22" "yarn": "^1.22.22"
} }
} }

View File

@ -1,6 +0,0 @@
node_modules
build
dist
tsconfig.tsbuildinfo
*.d.ts
*.js

View File

@ -1,35 +0,0 @@
{
"name": "@kittycad/codemirror-lsp-client",
"version": "1.0.0",
"description": "An LSP client for the codemirror editor.",
"main": "src/index.ts",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"scripts": {
"build": "tsc"
},
"types": "dist/index.d.ts",
"module": "dist/index.js",
"type": "module",
"repository": "https://github.com/KittyCAD/modeling-app",
"author": "Zoo Engineering Team",
"license": "MIT",
"private": false,
"dependencies": {
"@codemirror/autocomplete": "^6.16.3",
"@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.1",
"@lezer/highlight": "^1.2.0",
"@ts-stack/markdown": "^1.5.0",
"json-rpc-2.0": "^1.7.0",
"typescript": "^5.5.2",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8"
},
"devDependencies": {
"@types/node": "^20.14.9",
"ts-node": "^10.9.2"
}
}

View File

@ -1,13 +0,0 @@
import { Message } from 'vscode-languageserver-protocol'
export default class Tracer {
static client(message: string): void {
console.log('lsp client message', message)
}
static server(input: string | Message): void {
const message: string =
typeof input === 'string' ? input : JSON.stringify(input)
console.log('lsp server message', message)
}
}

View File

@ -1,57 +0,0 @@
import { foldService } from '@codemirror/language'
import { Extension, EditorState } from '@codemirror/state'
import { ViewPlugin } from '@codemirror/view'
import {
docPathFacet,
LanguageServerPlugin,
LanguageServerPluginSpec,
languageId,
workspaceFolders,
LanguageServerOptions,
} from './plugin/lsp'
export type { LanguageServerClientOptions } from './client'
export { LanguageServerClient } from './client'
export {
Codec,
FromServer,
IntoServer,
LspWorkerEventType,
} from './client/codec'
export type { LanguageServerOptions } from './plugin/lsp'
export {
LanguageServerPlugin,
LanguageServerPluginSpec,
docPathFacet,
languageId,
workspaceFolders,
lspSemanticTokensEvent,
lspDiagnosticsEvent,
lspFormatCodeEvent,
} from './plugin/lsp'
export { posToOffset, offsetToPos } from './plugin/util'
export function lspPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null
const viewPlugin = ViewPlugin.define(
(view) => (plugin = new LanguageServerPlugin(options, view)),
new LanguageServerPluginSpec()
)
let ext = [
docPathFacet.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
viewPlugin,
foldService.of((state: EditorState, lineStart: number, lineEnd: number) => {
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.
const range = plugin?.foldingRange(lineStart, lineEnd)
return range
}),
]
return ext
}

View File

@ -1,112 +0,0 @@
import {
acceptCompletion,
autocompletion,
clearSnippet,
closeCompletion,
hasNextSnippetField,
moveCompletionSelection,
nextSnippetField,
prevSnippetField,
startCompletion,
} from '@codemirror/autocomplete'
import { Prec, Extension } from '@codemirror/state'
import { EditorView, keymap, KeyBinding, ViewPlugin } from '@codemirror/view'
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])
) as Record<CompletionItemKind, string>
const lspAutocompleteKeymap: readonly KeyBinding[] = [
{ key: 'Ctrl-Space', run: startCompletion },
{
key: 'Escape',
run: (view: EditorView): boolean => {
if (clearSnippet(view)) return true
return closeCompletion(view)
},
},
{ key: 'ArrowDown', run: moveCompletionSelection(true) },
{ key: 'ArrowUp', run: moveCompletionSelection(false) },
{ key: 'PageDown', run: moveCompletionSelection(true, 'page') },
{ key: 'PageUp', run: moveCompletionSelection(false, 'page') },
{ key: 'Enter', run: acceptCompletion },
{
key: 'Tab',
run: (view: EditorView): boolean => {
if (hasNextSnippetField(view.state)) {
const result = nextSnippetField(view)
return result
}
return acceptCompletion(view)
},
shift: prevSnippetField,
},
]
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

@ -1,27 +0,0 @@
import { Extension, Prec } from '@codemirror/state'
import { EditorView, keymap, KeyBinding, ViewPlugin } from '@codemirror/view'
import { LanguageServerPlugin } from './lsp'
export default function lspFormatExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
const formatKeymap: readonly KeyBinding[] = [
{
key: 'Alt-Shift-f',
run: (view: EditorView) => {
let value = view.plugin(plugin)
if (!value) return false
value.requestFormatting()
return true
},
},
]
// Create an extension for the key mappings.
const formatKeymapExt = Prec.highest(
keymap.computeN([], () => [formatKeymap])
)
return formatKeymapExt
}

View File

@ -1,22 +0,0 @@
import { Extension } from '@codemirror/state'
import { hoverTooltip, tooltips, ViewPlugin } from '@codemirror/view'
import { LanguageServerPlugin } from './lsp'
import { offsetToPos } from './util'
export default function lspHoverExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
hoverTooltip((view, pos) => {
const value = view.plugin(plugin)
return (
value?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
null
)
}),
tooltips({
position: 'absolute',
}),
]
}

View File

@ -1,21 +0,0 @@
import { indentService } from '@codemirror/language'
import { Extension } from '@codemirror/state'
export default function lspIndentExt(): Extension {
// Match the indentation of the previous line (if present).
return indentService.of((context, pos) => {
try {
const previousLine = context.lineAt(pos, -1)
const previousLineText = previousLine.text.replaceAll(
'\t',
' '.repeat(context.state.tabSize)
)
const match = previousLineText.match(/^(\s)*/)
if (match === null || match.length <= 0) return null
return match[0].length
} catch (err) {
console.error('Error in codemirror indentService', err)
}
return null
})
}

View File

@ -1,12 +0,0 @@
import { Extension } from '@codemirror/state'
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
export default function lspLintExt(): Extension {
return linter((view) => {
let diagnostics: Diagnostic[] = []
forEachDiagnostic(view.state, (d: Diagnostic, from: number, to: number) => {
diagnostics.push(d)
})
return diagnostics
})
}

View File

@ -1,175 +0,0 @@
import { highlightingFor } from '@codemirror/language'
import { StateEffect, StateField, Extension } from '@codemirror/state'
import { EditorView, Decoration, DecorationSet } from '@codemirror/view'
import { Tag, tags } from '@lezer/highlight'
import { lspSemanticTokensEvent } from './lsp'
export interface SemanticToken {
from: number
to: number
type: string
modifiers: string[]
}
export const addToken = StateEffect.define<SemanticToken>({
map: (token: SemanticToken, change) => ({
...token,
from: change.mapPos(token.from),
to: change.mapPos(token.to),
}),
})
export default function lspSemanticTokenExt(): Extension {
return StateField.define<DecorationSet>({
create() {
return Decoration.none
},
update(highlights, tr) {
// Nothing can come before this line, this is very important!
// It makes sure the highlights are updated correctly for the changes.
highlights = highlights.map(tr.changes)
const isSemanticTokensEvent = tr.annotation(lspSemanticTokensEvent.type)
if (!isSemanticTokensEvent) {
return highlights
}
// Check if any of the changes are addToken
const hasAddToken = tr.effects.some((e) => e.is(addToken))
if (hasAddToken) {
highlights = highlights.update({
filter: (from, to) => false,
})
}
for (const e of tr.effects)
if (e.is(addToken)) {
const tag = getTag(e.value)
const className = tag
? highlightingFor(tr.startState, [tag])
: undefined
if (e.value.from < e.value.to && tag) {
if (className) {
highlights = highlights.update({
add: [
Decoration.mark({ class: className }).range(
e.value.from,
e.value.to
),
],
})
}
}
}
return highlights
},
provide: (f) => EditorView.decorations.from(f),
})
}
export function getTag(semanticToken: SemanticToken): Tag | null {
let tokenType = convertSemanticTokenTypeToCodeMirrorTag(semanticToken.type)
if (
semanticToken.modifiers === undefined ||
semanticToken.modifiers === null ||
semanticToken.modifiers.length === 0
) {
return tokenType
}
for (let modifier of semanticToken.modifiers) {
tokenType = convertSemanticTokenToCodeMirrorTag(
'',
modifier,
tokenType || undefined
)
}
return tokenType
}
export function getTagName(semanticToken: SemanticToken): string {
let tokenType = semanticToken.type
if (
semanticToken.modifiers === undefined ||
semanticToken.modifiers === null ||
semanticToken.modifiers.length === 0
) {
return tokenType
}
for (let modifier of semanticToken.modifiers) {
tokenType = `${tokenType}.${modifier}`
}
return tokenType
}
function convertSemanticTokenTypeToCodeMirrorTag(
tokenType: string
): Tag | null {
switch (tokenType) {
case 'keyword':
return tags.keyword
case 'variable':
return tags.variableName
case 'string':
return tags.string
case 'number':
return tags.number
case 'comment':
return tags.comment
case 'operator':
return tags.operator
case 'function':
return tags.function(tags.name)
case 'type':
return tags.typeName
case 'property':
return tags.propertyName
case 'parameter':
return tags.local(tags.name)
default:
console.error('Unknown token type:', tokenType)
return null
}
}
function convertSemanticTokenToCodeMirrorTag(
tokenType: string,
tokenModifier: string,
givenTag?: Tag
): Tag | null {
let tag = givenTag
? givenTag
: convertSemanticTokenTypeToCodeMirrorTag(tokenType)
if (!tag) {
return null
}
if (tokenModifier) {
switch (tokenModifier) {
case 'definition':
return tags.definition(tag)
case 'declaration':
return tags.definition(tag)
case 'readonly':
return tags.constant(tag)
case 'static':
return tags.constant(tag)
case 'defaultLibrary':
return tags.standard(tag)
default:
console.error('Unknown token modifier:', tokenModifier)
return tag
}
}
return tag
}

View File

@ -1,55 +0,0 @@
import { Text } from '@codemirror/state'
import { Marked } from '@ts-stack/markdown'
import type * as LSP from 'vscode-languageserver-protocol'
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
export function deferExecution<T>(func: (args: T) => any, wait: number) {
let timeout: ReturnType<typeof setTimeout> | null
let latestArgs: T
function later() {
timeout = null
func(latestArgs)
}
function deferred(args: T) {
latestArgs = args
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
}
return deferred
}
export function posToOffset(
doc: Text,
pos: { line: number; character: number }
): number | undefined {
if (pos.line >= doc.lines) return
const offset = doc.line(pos.line + 1).from + pos.character
if (offset > doc.length) return
return offset
}
export function offsetToPos(doc: Text, offset: number) {
const line = doc.lineAt(offset)
return {
line: line.number - 1,
character: offset - line.from,
}
}
export function formatMarkdownContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string {
if (Array.isArray(contents)) {
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
} else if (typeof contents === 'string') {
return Marked.parse(contents)
} else {
return Marked.parse(contents.value)
}
}

View File

@ -1,18 +0,0 @@
{
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src", "./*.ts"],
"exclude": ["node_modules"]
}

View File

@ -1,231 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@codemirror/autocomplete@^6.16.3":
version "6.16.3"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz#04d5a4e4e44ccae1ba525d47db53a5479bf46338"
integrity sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.2":
version "6.10.2"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61"
integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
"@lezer/common" "^1.1.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
"@codemirror/state@^6.0.0", "@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/view@^6.17.0", "@codemirror/view@^6.23.0":
version "6.28.2"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.28.2.tgz#026d5d2bd315aa015c1a1573b6358eeba7acd004"
integrity sha512-A3DmyVfjgPsGIjiJqM/zvODUAPQdQl3ci0ghehYNnbt5x+o76xq+dL5+mMBuysDXnI3kapgOkoeJ0sbtL/3qPw==
dependencies:
"@codemirror/state" "^6.4.0"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@jridgewell/resolve-uri@^3.0.3":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@lezer/common@^1.0.0", "@lezer/common@^1.1.0":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049"
integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780"
integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/lr@^1.0.0":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.1.tgz#fe25f051880a754e820b28148d90aa2a96b8bdd2"
integrity sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==
dependencies:
"@lezer/common" "^1.0.0"
"@ts-stack/markdown@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@ts-stack/markdown/-/markdown-1.5.0.tgz#5dc298a20dc3dc040143c5a5948201eb6bf5419d"
integrity sha512-ntVX2Kmb2jyTdH94plJohokvDVPvp6CwXHqsa9NVZTK8cOmHDCYNW0j6thIadUVRTStJhxhfdeovLd0owqDxLw==
dependencies:
tslib "^2.3.0"
"@tsconfig/node10@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==
"@tsconfig/node12@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/node@^20.14.9":
version "20.14.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420"
integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==
dependencies:
undici-types "~5.26.4"
acorn-walk@^8.1.1:
version "8.3.3"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e"
integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==
dependencies:
acorn "^8.11.0"
acorn@^8.11.0, acorn@^8.4.1:
version "8.12.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c"
integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
json-rpc-2.0@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/json-rpc-2.0/-/json-rpc-2.0-1.7.0.tgz#840deb0bc168463e12bceb462f7fe225e793fc17"
integrity sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg==
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
style-mod@^4.0.0, style-mod@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
tslib@^2.3.0:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
typescript@^5.5.2:
version "5.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507"
integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
vscode-jsonrpc@8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
vscode-languageserver-protocol@^3.17.5:
version "3.17.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea"
integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==
dependencies:
vscode-jsonrpc "8.2.0"
vscode-languageserver-types "3.17.5"
vscode-languageserver-types@3.17.5:
version "3.17.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
vscode-uri@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
w3c-keyname@^2.2.4:
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

View File

@ -15,8 +15,8 @@ export default defineConfig({
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Do not retry */ /* Retry on CI only */
retries: process.env.CI ? 0 : 0, retries: process.env.CI ? 3 : 0,
/* Different amount of parallelism on CI and local. */ /* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 4 : 4, workers: process.env.CI ? 4 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */

View File

@ -1,7 +1,6 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
'@csstools/postcss-oklab-function': { preserve: true },
autoprefixer: {}, autoprefixer: {},
}, },
} }

545
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.18", features = [] } tauri-build = { version = "2.0.0-beta.13", 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.23", features = [ "devtools", "unstable"] } tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.7" } tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-deep-link = { version = "2.0.0-beta.8" } tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
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.10" } tauri-plugin-fs = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.11" } tauri-plugin-http = { version = "2.0.0-beta.6" }
tauri-plugin-log = { version = "2.0.0-beta.7" } tauri-plugin-log = { version = "2.0.0-beta.4" }
tauri-plugin-os = { version = "2.0.0-beta.7" } tauri-plugin-os = { version = "2.0.0-beta.2" }
tauri-plugin-persisted-scope = { version = "2.0.0-beta.10" } tauri-plugin-persisted-scope = { version = "2.0.0-beta.7" }
tauri-plugin-process = { version = "2.0.0-beta.7" } tauri-plugin-process = { version = "2.0.0-beta.2" }
tauri-plugin-shell = { version = "2.0.0-beta.8" } tauri-plugin-shell = { version = "2.0.0-beta.2" }
tauri-plugin-updater = { version = "2.0.0-beta.9" } tauri-plugin-updater = { version = "2.0.0-beta.4" }
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,22 +63,16 @@
"subcommands": {} "subcommands": {}
}, },
"deep-link": { "deep-link": {
"mobile": [ "domains": [
{ {
"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.1" "version": "0.22.6"
} }

View File

@ -1,5 +1,6 @@
import { MouseEventHandler, useEffect, useMemo, useRef } from 'react' import { MouseEventHandler, useEffect, useRef } from 'react'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { useStore } from './useStore'
import { useHotKeyListener } from './hooks/useHotKeyListener' import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream' import { Stream } from './components/Stream'
import { EngineCommand } from './lang/std/engineConnection' import { EngineCommand } from './lang/std/engineConnection'
@ -43,15 +44,22 @@ export function App() {
}, [projectName, projectPath]) }, [projectName, projectPath])
useHotKeyListener() useHotKeyListener()
const { context } = useModelingContext() const { buttonDownInStream, didDragInStream, streamDimensions, setHtmlRef } =
useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
didDragInStream: s.didDragInStream,
streamDimensions: s.streamDimensions,
setHtmlRef: s.setHtmlRef,
}))
useEffect(() => {
setHtmlRef(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 = new CoreDumpManager(engineCommandManager, ref, token)
() => new CoreDumpManager(engineCommandManager, token),
[]
)
const { const {
app: { onboardingStatus }, app: { onboardingStatus },
@ -73,7 +81,7 @@ export function App() {
(p) => p === onboardingStatus.current (p) => p === onboardingStatus.current
) )
? 'opacity-20' ? 'opacity-20'
: context.store?.didDragInStream : didDragInStream
? 'opacity-40' ? 'opacity-40'
: '' : ''
@ -91,11 +99,11 @@ export function App() {
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,
el: e.currentTarget, el: e.currentTarget,
...context.store?.streamDimensions, ...streamDimensions,
}) })
const newCmdId = uuidv4() const newCmdId = uuidv4()
if (context.store?.buttonDownInStream === undefined) { if (buttonDownInStream === undefined) {
debounceSocketSend({ debounceSocketSend({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -117,7 +125,7 @@ export function App() {
className={ className={
'transition-opacity transition-duration-75 ' + 'transition-opacity transition-duration-75 ' +
paneOpacity + paneOpacity +
(context.store?.buttonDownInStream ? ' pointer-events-none' : '') (buttonDownInStream ? ' pointer-events-none' : '')
} }
project={{ project, file }} project={{ project, file }}
enableMenu={true} enableMenu={true}

View File

@ -1,41 +0,0 @@
import { createContext, useContext, useState, ReactNode } from 'react'
/*
This is for a very small handful of global state we need that doesn't fit into
any of the Xstate machines.
Please do not fill this up with junk.
*/
interface AppState {
isStreamReady: boolean
setAppState: (newAppState: Partial<AppState>) => void
}
const AppStateContext = createContext<AppState>({
isStreamReady: false,
setAppState: () => {},
})
export const useAppState = () => useContext(AppStateContext)
export const AppStateProvider = ({ children }: { children: ReactNode }) => {
const [appState, _setAppState] = useState<AppState>({
isStreamReady: false,
setAppState: () => {},
})
const setAppState = (newAppState: Partial<AppState>) =>
_setAppState({ ...appState, ...newAppState })
return (
<AppStateContext.Provider
value={{
isStreamReady: appState.isStreamReady,
setAppState,
}}
>
{children}
</AppStateContext.Provider>
)
}

View File

@ -33,14 +33,6 @@ import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider' import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants' import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/tauri' import { getState, setState } from 'lib/tauri'
import { CoreDumpManager } from 'lib/coredump'
import { engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm'
import { useMemo } from 'react'
import { AppStateProvider } from 'AppState'
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -53,9 +45,7 @@ const router = createBrowserRouter([
<SettingsAuthProvider> <SettingsAuthProvider>
<LspProvider> <LspProvider>
<KclContextProvider> <KclContextProvider>
<AppStateProvider> <Outlet />
<Outlet />
</AppStateProvider>
</KclContextProvider> </KclContextProvider>
</LspProvider> </LspProvider>
</SettingsAuthProvider> </SettingsAuthProvider>
@ -97,7 +87,6 @@ const router = createBrowserRouter([
<Auth> <Auth>
<FileMachineProvider> <FileMachineProvider>
<ModelingMachineProvider> <ModelingMachineProvider>
<CoreDump />
<Outlet /> <Outlet />
<App /> <App />
<CommandBar /> <CommandBar />
@ -176,30 +165,3 @@ export const Router = () => {
</NetworkContext.Provider> </NetworkContext.Provider>
) )
} }
function CoreDump() {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, token),
[]
)
useHotkeyWrapper(['meta + shift + .'], () => {
toast.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
})
return null
}

View File

@ -8,14 +8,10 @@ import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst' import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useStore } from 'useStore'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown' 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 {
canRectangleTool,
isEditingExistingSketch,
} from 'machines/modelingMachine'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -42,56 +38,38 @@ export function Toolbar({
const toolbarButtonsRef = useRef<HTMLUListElement>(null) const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState() const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
}))
const disableAllButtons = const disableAllButtons =
(overallState !== NetworkHealthState.Ok && (overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) || overallState !== NetworkHealthState.Weak) ||
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({ : send('Equip Line tool'),
type: 'change tool', { enabled: !disableAllButtons, scopes: ['sketch'] }
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({ : send('Equip tangential arc to'),
type: 'change tool', { enabled: !disableAllButtons, scopes: ['sketch'] }
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({ : send('Equip rectangle tool'),
type: 'change tool', { enabled: !disableAllButtons, scopes: ['sketch'] }
data: 'rectangle',
}),
{ enabled: !disableRectangle, scopes: ['sketch'] }
) )
useHotkeys( useHotkeys(
's', 's',
@ -104,7 +82,7 @@ export function Toolbar({
useHotkeys( useHotkeys(
'esc', 'esc',
() => () =>
['Sketch no face', 'Sketch.SketchIdle'].some(state.matches) state.matches('Sketch.SketchIdle')
? send('Cancel') ? send('Cancel')
: send('CancelSketch'), : send('CancelSketch'),
{ enabled: !disableAllButtons, scopes: ['sketch'] } { enabled: !disableAllButtons, scopes: ['sketch'] }
@ -247,11 +225,6 @@ 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">
@ -261,10 +234,7 @@ export function Toolbar({
onClick={() => onClick={() =>
state?.matches('Sketch.Line tool') state?.matches('Sketch.Line tool')
? send('CancelSketch') ? send('CancelSketch')
: send({ : send('Equip Line tool')
type: 'change tool',
data: 'line',
})
} }
aria-pressed={state?.matches('Sketch.Line tool')} aria-pressed={state?.matches('Sketch.Line tool')}
iconStart={{ iconStart={{
@ -272,7 +242,7 @@ export function Toolbar({
iconClassName, iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableLineButton} disabled={disableAllButtons}
> >
Line Line
<Tooltip <Tooltip
@ -291,10 +261,7 @@ export function Toolbar({
onClick={() => onClick={() =>
state.matches('Sketch.Tangential arc to') state.matches('Sketch.Tangential arc to')
? send('CancelSketch') ? send('CancelSketch')
: send({ : send('Equip tangential arc to')
type: 'change tool',
data: 'tangentialArc',
})
} }
aria-pressed={state.matches('Sketch.Tangential arc to')} aria-pressed={state.matches('Sketch.Tangential arc to')}
iconStart={{ iconStart={{
@ -302,7 +269,11 @@ export function Toolbar({
iconClassName, iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableTangentialArc} disabled={
(!state.can('Equip tangential arc to') &&
!state.matches('Sketch.Tangential arc to')) ||
disableAllButtons
}
> >
Tangential Arc Tangential Arc
<Tooltip <Tooltip
@ -321,10 +292,7 @@ export function Toolbar({
onClick={() => onClick={() =>
state.matches('Sketch.Rectangle tool') state.matches('Sketch.Rectangle tool')
? send('CancelSketch') ? send('CancelSketch')
: send({ : send('Equip rectangle tool')
type: 'change tool',
data: 'rectangle',
})
} }
aria-pressed={state.matches('Sketch.Rectangle tool')} aria-pressed={state.matches('Sketch.Rectangle tool')}
iconStart={{ iconStart={{
@ -332,9 +300,13 @@ export function Toolbar({
iconClassName, iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableRectangle} disabled={
(!state.can('Equip rectangle tool') &&
!state.matches('Sketch.Rectangle tool')) ||
disableAllButtons
}
title={ title={
canRectangleTool(context) state.can('Equip rectangle tool')
? 'Rectangle' ? 'Rectangle'
: 'Can only be used when a sketch is empty currently' : 'Can only be used when a sketch is empty currently'
} }

View File

@ -74,9 +74,6 @@ export class CameraControls {
enableRotate = true enableRotate = true
enablePan = true enablePan = true
enableZoom = true enableZoom = true
zoomDataFromLastFrame?: number = undefined
// holds coordinates, and interaction
moveDataFromLastFrame?: [number, number, string] = undefined
lastPerspectiveFov: number = 45 lastPerspectiveFov: number = 45
pendingZoom: number | null = null pendingZoom: number | null = null
pendingRotation: Vector2 | null = null pendingRotation: Vector2 | null = null
@ -104,12 +101,16 @@ export class CameraControls {
get isPerspective() { get isPerspective() {
return this.camera instanceof PerspectiveCamera return this.camera instanceof PerspectiveCamera
} }
private debounceTimer = 0
handleStart = () => { handleStart = () => {
if (this.debounceTimer) clearTimeout(this.debounceTimer)
this._isCamMovingCallback(true, false) this._isCamMovingCallback(true, false)
} }
handleEnd = () => { handleEnd = () => {
this._isCamMovingCallback(false, false) this.debounceTimer = setTimeout(() => {
this._isCamMovingCallback(false, false)
}, 400) as any as number
} }
setCam = (camProps: ReactCameraProperties) => { setCam = (camProps: ReactCameraProperties) => {
@ -229,7 +230,6 @@ export class CameraControls {
camSettings.orientation.z, camSettings.orientation.z,
camSettings.orientation.w camSettings.orientation.w
).invert() ).invert()
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat)) this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) { if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
this.useOrthographicCamera() this.useOrthographicCamera()
@ -258,48 +258,6 @@ export class CameraControls {
} }
this.onCameraChange() this.onCameraChange()
} }
// Our stream is never more than 60fps.
// We can get away with capping our "virtual fps" to 60 then.
const FPS_VIRTUAL = 60
const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart()
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude:
(-1 * this.zoomDataFromLastFrame) / window.devicePixelRatio,
},
cmd_id: uuidv4(),
})
this.handleEnd()
}
this.zoomDataFromLastFrame = undefined
}
setInterval(doZoom, 1000 / FPS_VIRTUAL)
const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) {
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: this.moveDataFromLastFrame[2] as any,
window: {
x: this.moveDataFromLastFrame[0],
y: this.moveDataFromLastFrame[1],
},
},
cmd_id: uuidv4(),
})
}
this.moveDataFromLastFrame = undefined
}
setInterval(doMove, 1000 / FPS_VIRTUAL)
setTimeout(() => { setTimeout(() => {
this.engineCommandManager.subscribeTo({ this.engineCommandManager.subscribeTo({
event: 'camera_drag_end', event: 'camera_drag_end',
@ -384,7 +342,15 @@ export class CameraControls {
if (interaction === 'none') return if (interaction === 'none') return
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction] this.throttledEngCmd({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction,
window: { x: event.clientX, y: event.clientY },
},
cmd_id: uuidv4(),
})
return return
} }
@ -432,19 +398,34 @@ export class CameraControls {
} }
onMouseWheel = (event: WheelEvent) => { onMouseWheel = (event: WheelEvent) => {
// Assume trackpad if the deltas are small and integers
this.handleStart()
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
this.zoomDataFromLastFrame = event.deltaY const interactions = this.interactionGuards.zoom.scrollCallback(
event as any
)
if (!interactions) {
this.handleEnd()
return
}
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: -event.deltaY * 0.4,
},
cmd_id: uuidv4(),
})
this.handleEnd()
return return
} }
// else "clientToEngine" (Sketch Mode) or forceUpdate // Else "clientToEngine" (Sketch Mode) or forceUpdate
// We need to simulate similar behavior as when we send
// zoom commands to engine. This means dropping some zoom
// commands too.
// From onMouseMove zoom handling which seems to be really smooth // From onMouseMove zoom handling which seems to be really smooth
this.handleStart() this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001 this.pendingZoom *= 1 + event.deltaY * 0.01
this.handleEnd() this.handleEnd()
} }

View File

@ -37,7 +37,7 @@ import { Dialog, Popover, Transition } from '@headlessui/react'
import { LineInputsType } from 'lang/std/sketchcombos' import { LineInputsType } from 'lang/std/sketchcombos'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { InstanceProps, create } from 'react-modal-promise' import { InstanceProps, create } from 'react-modal-promise'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'useStore'
import { import {
deleteSegmentFromPipeExpression, deleteSegmentFromPipeExpression,
makeRemoveSingleConstraintInput, makeRemoveSingleConstraintInput,

View File

@ -58,7 +58,7 @@ import {
editorManager, editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst } from 'lang/langHelpers' import { executeAst, useStore } from 'useStore'
import { import {
createArcGeometry, createArcGeometry,
dashedStraight, dashedStraight,
@ -534,7 +534,7 @@ export class SceneEntities {
segmentName: 'line' | 'tangentialArcTo' = 'line', segmentName: 'line' | 'tangentialArcTo' = 'line',
shouldTearDown = true shouldTearDown = true
) => { ) => {
const _ast = JSON.parse(JSON.stringify(kclManager.ast)) const _ast = kclManager.ast
const _node1 = getNodeFromPath<VariableDeclaration>( const _node1 = getNodeFromPath<VariableDeclaration>(
_ast, _ast,
@ -568,7 +568,6 @@ export class SceneEntities {
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride, sketchGroup } = const { truncatedAst, programMemoryOverride, sketchGroup } =
await this.setupSketch({ await this.setupSketch({
sketchPathToNode, sketchPathToNode,
@ -693,7 +692,7 @@ export class SceneEntities {
sketchOrigin: [number, number, number], sketchOrigin: [number, number, number],
rectangleOrigin: [x: number, y: number] rectangleOrigin: [x: number, y: number]
) => { ) => {
let _ast = JSON.parse(JSON.stringify(kclManager.ast)) let _ast = kclManager.ast
const _node1 = getNodeFromPath<VariableDeclaration>( const _node1 = getNodeFromPath<VariableDeclaration>(
_ast, _ast,
@ -724,7 +723,9 @@ export class SceneEntities {
...getRectangleCallExpressions(rectangleOrigin, tags), ...getRectangleCallExpressions(rectangleOrigin, tags),
]) ])
_ast = parse(recast(_ast)) let result = parse(recast(_ast))
if (trap(result)) return Promise.reject(result)
_ast = result
const { programMemoryOverride, truncatedAst } = await this.setupSketch({ const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode, sketchPathToNode,
@ -738,7 +739,7 @@ export class SceneEntities {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onMove: async (args) => { onMove: async (args) => {
// Update the width and height of the draft rectangle // Update the width and height of the draft rectangle
const pathToNodeTwo = JSON.parse(JSON.stringify(sketchPathToNode)) const pathToNodeTwo = sketchPathToNode
pathToNodeTwo[1][0] = 0 pathToNodeTwo[1][0] = 0
const _node = getNodeFromPath<VariableDeclaration>( const _node = getNodeFromPath<VariableDeclaration>(
@ -800,11 +801,13 @@ export class SceneEntities {
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type === 'PipeExpression') {
updateRectangleSketch(sketchInit, x, y, tags[0]) updateRectangleSketch(sketchInit, x, y, tags[0])
_ast = parse(recast(_ast)) let result = parse(recast(_ast))
if (trap(result)) return Promise.reject(result)
_ast = result
// 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: 'Finish rectangle' }) sceneInfra.modelingSend({ type: 'CancelSketch' })
const { programMemory } = await executeAst({ const { programMemory } = await executeAst({
ast: _ast, ast: _ast,
@ -1004,10 +1007,8 @@ export class SceneEntities {
PROFILE_START, PROFILE_START,
]) ])
if (!group) return if (!group) return
const pathToNode: PathToNode = JSON.parse( const pathToNode: PathToNode = group.userData.pathToNode
JSON.stringify(group.userData.pathToNode) const varDecIndex: number = pathToNode[1][0] as number
)
const varDecIndex = JSON.parse(JSON.stringify(pathToNode[1][0]))
if (draftInfo) { if (draftInfo) {
pathToNode[1][0] = 0 pathToNode[1][0] = 0
} }
@ -1444,10 +1445,11 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type) selected.material.color = defaultPlaneColor(type)
}, },
onClick: async (args) => { onClick: async (args) => {
const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine( const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent, args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement, document.getElementById('video-stream') as HTMLVideoElement,
sceneInfra._streamDimensions streamDimensions
) )
let _entity_id = entity_id let _entity_id = entity_id
@ -1719,7 +1721,7 @@ function prepareTruncatedMemoryAndAst(
} }
| Error { | Error {
const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0 const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0
const _ast = JSON.parse(JSON.stringify(ast)) const _ast = ast
const _node = getNodeFromPath<VariableDeclaration>( const _node = getNodeFromPath<VariableDeclaration>(
_ast, _ast,
@ -1778,7 +1780,7 @@ function prepareTruncatedMemoryAndAst(
} }
const truncatedAst: Program = { const truncatedAst: Program = {
..._ast, ..._ast,
body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))], body: [_ast.body[bodyIndex]],
} }
const programMemoryOverride = programMemoryInit() const programMemoryOverride = programMemoryInit()
if (err(programMemoryOverride)) return programMemoryOverride if (err(programMemoryOverride)) return programMemoryOverride
@ -1804,7 +1806,7 @@ function prepareTruncatedMemoryAndAst(
} }
if (value.type === 'TagIdentifier') { if (value.type === 'TagIdentifier') {
programMemoryOverride.root[key] = JSON.parse(JSON.stringify(value)) programMemoryOverride.root[key] = value
} }
} }
@ -1819,7 +1821,7 @@ function prepareTruncatedMemoryAndAst(
if (!memoryItem) { if (!memoryItem) {
continue continue
} }
programMemoryOverride.root[name] = JSON.parse(JSON.stringify(memoryItem)) programMemoryOverride.root[name] = memoryItem
} }
return { return {
truncatedAst, truncatedAst,

View File

@ -103,10 +103,6 @@ export class SceneInfra {
_baseUnit: BaseUnit = 'mm' _baseUnit: BaseUnit = 'mm'
_baseUnitMultiplier = 1 _baseUnitMultiplier = 1
_theme: Themes = Themes.System _theme: Themes = Themes.System
_streamDimensions: { streamWidth: number; streamHeight: number } = {
streamWidth: 1280,
streamHeight: 720,
}
extraSegmentTexture: Texture extraSegmentTexture: Texture
lastMouseState: MouseState = { type: 'idle' } lastMouseState: MouseState = { type: 'idle' }
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {} onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}

View File

@ -26,7 +26,6 @@ export const AppHeader = ({
return ( return (
<header <header
id="app-header"
className={ className={
'w-full grid ' + 'w-full grid ' +
styles.header + styles.header +

View File

@ -10,7 +10,7 @@ import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'useStore'
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
export const AvailableVars = ({ export const AvailableVars = ({
@ -151,9 +151,7 @@ export function useCalc({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: kclManager.programMemory,
JSON.stringify(kclManager.programMemory)
),
}).then(({ programMemory }) => { }).then(({ programMemory }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>

View File

@ -24,7 +24,7 @@ export const CommandBar = () => {
}, [pathname]) }, [pathname])
// Hook up keyboard shortcuts // Hook up keyboard shortcuts
useHotkeyWrapper(['mod+k'], () => { useHotkeyWrapper(['mod+k', 'ctrl+c'], () => {
if (commandBarState.context.commands.length === 0) return if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) { if (commandBarState.matches('Closed')) {
commandBarSend({ type: 'Open' }) commandBarSend({ type: 'Open' })

View File

@ -1,6 +1,5 @@
import { Completion } from '@codemirror/autocomplete' import { Completion } from '@codemirror/autocomplete'
import { EditorView, ViewUpdate } from '@codemirror/view' import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror'
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'
@ -13,7 +12,6 @@ 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,
@ -65,7 +63,9 @@ function CommandBarKclInput({
const { setContainer } = useCodeMirror({ const { setContainer } = useCodeMirror({
container: editorRef.current, container: editorRef.current,
initialDocValue: value, value,
indentWithTab: false,
basicSetup: false,
autoFocus: true, autoFocus: true,
selection: { selection: {
anchor: 0, anchor: 0,
@ -74,6 +74,7 @@ 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()
@ -95,12 +96,8 @@ 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,11 +175,7 @@ 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

@ -1,15 +1,18 @@
import { LanguageServerClient } from 'editor/plugins/lsp'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import React, { createContext, useMemo, useContext, useState } from 'react' import React, {
import { createContext,
LanguageServerClient, useMemo,
FromServer, useEffect,
IntoServer, useContext,
LspWorkerEventType, useState,
LanguageServerPlugin, } from 'react'
} from '@kittycad/codemirror-lsp-client' import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Client from '../editor/plugins/lsp/client'
import { TEST, VITE_KC_API_BASE_URL } from 'env' import { TEST, VITE_KC_API_BASE_URL } from 'env'
import KclLanguageSupport from 'editor/plugins/lsp/kcl/language' import kclLanguage from 'editor/plugins/lsp/kcl/language'
import { copilotPlugin } from 'editor/plugins/lsp/copilot' import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { useStore } from 'useStore'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Extension } from '@codemirror/state' import { Extension } from '@codemirror/state'
import { LanguageSupport } from '@codemirror/language' import { LanguageSupport } from '@codemirror/language'
@ -18,15 +21,16 @@ import { paths } from 'lib/paths'
import { FileEntry } from 'lib/types' import { FileEntry } from 'lib/types'
import Worker from 'editor/plugins/lsp/worker.ts?worker' import Worker from 'editor/plugins/lsp/worker.ts?worker'
import { import {
LspWorkerEventType,
KclWorkerOptions, KclWorkerOptions,
CopilotWorkerOptions, CopilotWorkerOptions,
LspWorker, LspWorker,
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { wasmUrl } from 'lang/wasm' import { wasmUrl } from 'lang/wasm'
import { PROJECT_ENTRYPOINT } from 'lib/constants' import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { isTauri } from 'lib/isTauri'
import { codeManager } from 'lib/singletons'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -66,8 +70,21 @@ type LspContext = {
export const LspStateContext = createContext({} as LspContext) export const LspStateContext = createContext({} as LspContext)
export const LspProvider = ({ children }: { children: React.ReactNode }) => { export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false) const {
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false) isKclLspServerReady,
isCopilotLspServerReady,
setIsKclLspServerReady,
setIsCopilotLspServerReady,
isStreamReady,
} = useStore((s) => ({
isKclLspServerReady: s.isKclLspServerReady,
isCopilotLspServerReady: s.isCopilotLspServerReady,
setIsKclLspServerReady: s.setIsKclLspServerReady,
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
isStreamReady: s.isStreamReady,
}))
const [isLspReady, setIsLspReady] = useState(false)
const [isCopilotReady, setIsCopilotReady] = useState(false)
const { const {
auth, auth,
@ -79,6 +96,8 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
} = useSettingsAuthContext() } = useSettingsAuthContext()
const token = auth?.context.token const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
const { overallState } = useNetworkContext()
const isNetworkOkay = overallState === NetworkHealthState.Ok
// So this is a bit weird, we need to initialize the lsp server and client. // So this is a bit weird, we need to initialize the lsp server and client.
// But the server happens async so we break this into two parts. // But the server happens async so we break this into two parts.
@ -109,34 +128,17 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const fromServer: FromServer | Error = FromServer.create() const fromServer: FromServer | Error = FromServer.create()
if (err(fromServer)) return { lspClient: null } if (err(fromServer)) return { lspClient: null }
const lspClient = new LanguageServerClient({ const client = new Client(fromServer, intoServer)
name: LspWorker.Kcl,
fromServer,
intoServer,
initializedCallback: () => {
setIsKclLspReady(true)
},
})
setIsLspReady(true)
const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl })
return { lspClient } return { lspClient }
}, [ }, [
// We need a token for authenticating the server. // We need a token for authenticating the server.
token, token,
]) ])
useMemo(() => {
if (!isTauri() && isKclLspReady && kclLspClient && codeManager.code) {
kclLspClient.textDocumentDidOpen({
textDocument: {
uri: `file:///${PROJECT_ENTRYPOINT}`,
languageId: 'kcl',
version: 1,
text: codeManager.code,
},
})
}
}, [kclLspClient, isKclLspReady])
// Here we initialize the plugin which will start the client. // Here we initialize the plugin which will start the client.
// Now that we have multi-file support the name of the file is a dep of // Now that we have multi-file support the name of the file is a dep of
// this use memo, as well as the directory structure, which I think is // this use memo, as well as the directory structure, which I think is
@ -144,38 +146,39 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => { const kclLSP = useMemo(() => {
let plugin = null let plugin = null
if (isKclLspReady && !TEST && kclLspClient) { if (isKclLspServerReady && !TEST && kclLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = new KclLanguageSupport({ const lsp = kclLanguage({
documentUri: `file:///${PROJECT_ENTRYPOINT}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,
workspaceFolders: getWorkspaceFolders(), workspaceFolders: getWorkspaceFolders(),
client: kclLspClient, client: kclLspClient,
processLspNotification: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => {
try {
switch (notification.method) {
case 'kcl/astUpdated':
// Update the folding ranges, since the AST has changed.
// This is a hack since codemirror does not support async foldService.
// When they do we can delete this.
plugin.updateFoldingRanges()
plugin.requestSemanticTokens()
break
case 'kcl/memoryUpdated':
break
}
} catch (error) {
console.error(error)
}
},
}) })
plugin = lsp plugin = lsp
} }
return plugin return plugin
}, [kclLspClient, isKclLspReady]) }, [kclLspClient, isKclLspServerReady])
// Re-execute the scene when the units change.
useEffect(() => {
if (kclLspClient) {
let plugins = kclLspClient.plugins
for (let plugin of plugins) {
if (plugin.updateUnits && isStreamReady && isNetworkOkay) {
plugin.updateUnits(defaultUnit.current)
}
}
}
}, [
kclLspClient,
defaultUnit.current,
// We want to re-execute the scene if the network comes back online.
// The lsp server will only re-execute if there were previous errors or
// changes, so it's fine to send it thru here.
isStreamReady,
isNetworkOkay,
])
const { lspClient: copilotLspClient } = useMemo(() => { const { lspClient: copilotLspClient } = useMemo(() => {
if (!token || token === '' || TEST) { if (!token || token === '' || TEST) {
@ -202,13 +205,13 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const fromServer: FromServer | Error = FromServer.create() const fromServer: FromServer | Error = FromServer.create()
if (err(fromServer)) return { lspClient: null } if (err(fromServer)) return { lspClient: null }
const client = new Client(fromServer, intoServer)
setIsCopilotReady(true)
const lspClient = new LanguageServerClient({ const lspClient = new LanguageServerClient({
client,
name: LspWorker.Copilot, name: LspWorker.Copilot,
fromServer,
intoServer,
initializedCallback: () => {
setIsCopilotLspReady(true)
},
}) })
return { lspClient } return { lspClient }
}, [token]) }, [token])
@ -220,7 +223,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
const copilotLSP = useMemo(() => { const copilotLSP = useMemo(() => {
let plugin = null let plugin = null
if (isCopilotLspReady && !TEST && copilotLspClient) { if (isCopilotLspServerReady && !TEST && copilotLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = copilotPlugin({ const lsp = copilotPlugin({
documentUri: `file:///${PROJECT_ENTRYPOINT}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,
@ -232,7 +235,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
plugin = lsp plugin = lsp
} }
return plugin return plugin
}, [copilotLspClient, isCopilotLspReady]) }, [copilotLspClient, isCopilotLspServerReady])
let lspClients: LanguageServerClient[] = [] let lspClients: LanguageServerClient[] = []
if (kclLspClient) { if (kclLspClient) {
@ -242,6 +245,13 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
lspClients.push(copilotLspClient) lspClients.push(copilotLspClient)
} }
useEffect(() => {
setIsKclLspServerReady(isLspReady)
}, [isLspReady])
useEffect(() => {
setIsCopilotLspServerReady(isCopilotReady)
}, [isCopilotReady])
const onProjectClose = ( const onProjectClose = (
file: FileEntry | null, file: FileEntry | null,
projectPath: string | null, projectPath: string | null,

View File

@ -30,6 +30,7 @@ import {
applyConstraintAngleBetween, applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween' } from './Toolbar/SetAngleBetween'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength' import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { useStore } from 'useStore'
import { import {
Selections, Selections,
canExtrudeSelection, canExtrudeSelection,
@ -53,7 +54,13 @@ import {
sketchOnExtrudedFace, sketchOnExtrudedFace,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { Program, VariableDeclaration, parse, recast } from 'lang/wasm' import {
Program,
VariableDeclaration,
coreDump,
parse,
recast,
} from 'lang/wasm'
import { import {
getNodeFromPath, getNodeFromPath,
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -64,14 +71,15 @@ 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 '@codemirror/state' import { EditorSelection } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump'
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'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -93,7 +101,7 @@ export const ModelingMachineProvider = ({
settings: { settings: {
context: { context: {
app: { theme, enableSSAO }, app: { theme, enableSSAO },
modeling: { defaultUnit, highlightEdges, showScaleGrid }, modeling: { defaultUnit, highlightEdges },
}, },
}, },
} = useSettingsAuthContext() } = useSettingsAuthContext()
@ -103,6 +111,37 @@ export const ModelingMachineProvider = ({
let [searchParams] = useSearchParams() let [searchParams] = useSearchParams()
const pool = searchParams.get('pool') const pool = searchParams.get('pool')
useSetupEngineManager(streamRef, token, {
pool: pool,
theme: theme.current,
highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,
})
const { htmlRef } = useStore((s) => ({
htmlRef: s.htmlRef,
}))
const coreDumpManager = new CoreDumpManager(
engineCommandManager,
htmlRef,
token
)
useHotkeyWrapper(['meta + shift + .'], () => {
toast.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
})
const { commandBarState } = useCommandsContext() const { commandBarState } = useCommandsContext()
// Settings machine setup // Settings machine setup
@ -122,13 +161,7 @@ export const ModelingMachineProvider = ({
modelingMachine, modelingMachine,
{ {
actions: { actions: {
'disable copilot': () => { 'sketch exit execute': () => {
editorManager.setCopilotEnabled(false)
},
'enable copilot': () => {
editorManager.setCopilotEnabled(true)
},
'sketch exit execute': ({ store }) => {
;(async () => { ;(async () => {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine() await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
@ -162,10 +195,7 @@ export const ModelingMachineProvider = ({
}) })
} }
store.videoElement?.pause() kclManager.executeCode(true)
kclManager.executeCode(true).then(() => {
store.videoElement?.play()
})
})() })()
}, },
'Set mouse state': assign({ 'Set mouse state': assign({
@ -251,15 +281,11 @@ export const ModelingMachineProvider = ({
const dispatchSelection = (selection?: EditorSelection) => { const dispatchSelection = (selection?: EditorSelection) => {
if (!selection) return // TODO less of hack for the below please if (!selection) return // TODO less of hack for the below please
if (!editorManager.editorView) return if (!editorManager.editorView) return
editorManager.lastSelectionEvent = Date.now()
setTimeout(() => { setTimeout(() => {
if (!editorManager.editorView) return if (editorManager.editorView) {
editorManager.editorView.dispatch({ editorManager.editorView.dispatch({ selection })
selection, }
annotations: [
modelingMachineEvent,
Transaction.addToHistory.of(false),
],
})
}) })
} }
let selections: Selections = { let selections: Selections = {
@ -302,6 +328,11 @@ export const ModelingMachineProvider = ({
) )
updateSceneObjectColors() updateSceneObjectColors()
// side effect to stop code mirror from updating the same selections again
editorManager.lastSelection = selections.codeBasedSelections
.map(({ range }) => `${range[1]}->${range[1]}`)
.join('&')
return { return {
selectionRanges: selections, selectionRanges: selections,
} }
@ -441,6 +472,17 @@ 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))
@ -471,15 +513,11 @@ export const ModelingMachineProvider = ({
services: { services: {
'AST-undo-startSketchOn': async ({ sketchDetails }) => { 'AST-undo-startSketchOn': async ({ sketchDetails }) => {
if (!sketchDetails) return if (!sketchDetails) return
if (kclManager.ast.body.length) { const newAst: Program = kclManager.ast
// this assumes no changes have been made to the sketch besides what we did when entering the sketch const varDecIndex = sketchDetails.sketchPathToNode[1][0]
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? // remove body item at varDecIndex
const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast)) newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
const varDecIndex = sketchDetails.sketchPathToNode[1][0] await kclManager.executeAstMock(newAst)
// remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst)
}
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: () => {}, onClick: () => {},
onDrag: () => {}, onDrag: () => {},
@ -859,16 +897,6 @@ export const ModelingMachineProvider = ({
} }
) )
useSetupEngineManager(streamRef, token, {
pool: pool,
theme: theme.current,
highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,
modelingSend,
modelingContext: modelingState.context,
showScaleGrid: showScaleGrid.current,
})
useEffect(() => { useEffect(() => {
kclManager.registerExecuteCallback(() => { kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' }) modelingSend({ type: 'Re-execute' })

View File

@ -1,6 +1,6 @@
import { useStore } from 'useStore'
import styles from './ModelingPane.module.css' import styles from './ModelingPane.module.css'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext'
export interface ModelingPaneProps export interface ModelingPaneProps
extends React.PropsWithChildren, extends React.PropsWithChildren,
@ -33,9 +33,11 @@ export const ModelingPane = ({
}: ModelingPaneProps) => { }: ModelingPaneProps) => {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboardingStatus
const { context } = useModelingContext() const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
const pointerEventsCssClass = const pointerEventsCssClass =
context.store?.buttonDownInStream || onboardingStatus.current === 'camera' buttonDownInStream || onboardingStatus.current === 'camera'
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto ' : 'pointer-events-auto '
return ( return (

View File

@ -1,171 +0,0 @@
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,3 +1,4 @@
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'
@ -42,7 +43,6 @@ import {
closeBracketsKeymap, closeBracketsKeymap,
completionKeymap, completionKeymap,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import CodeEditor from './CodeEditor'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -84,10 +84,6 @@ export const KclEditorPane = () => {
const textWrapping = context.textEditor.textWrapping const textWrapping = context.textEditor.textWrapping
const cursorBlinking = context.textEditor.blinkingCursor const cursorBlinking = context.textEditor.blinkingCursor
// DO NOT ADD THE CODEMIRROR HOTKEYS HERE TO THE DEPENDENCY ARRAY
// It reloads the editor every time we do _anything_ in the editor
// I have no idea why.
// Instead, hot load hotkeys via code mirror native.
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys() const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
const editorExtensions = useMemo(() => { const editorExtensions = useMemo(() => {
@ -138,6 +134,7 @@ export const KclEditorPane = () => {
highlightSelectionMatches(), highlightSelectionMatches(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }), syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
rectangularSelection(), rectangularSelection(),
drawSelection(),
dropCursor(), dropCursor(),
interact({ interact({
rules: [ rules: [
@ -176,7 +173,13 @@ export const KclEditorPane = () => {
} }
return extensions return extensions
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current]) }, [
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
codeMirrorHotkeys,
])
const initialCode = useRef(codeManager.code) const initialCode = useRef(codeManager.code)
@ -185,15 +188,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' : '')}
> >
<CodeEditor <ReactCodeMirror
initialDocValue={initialCode.current} value={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

@ -1,7 +1,8 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { HTMLAttributes, useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useStore } from 'useStore'
import { Tab } from '@headlessui/react' import { Tab } from '@headlessui/react'
import { import {
SidebarPane, SidebarPane,
@ -14,7 +15,6 @@ import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css' import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane' import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useModelingContext } from 'hooks/useModelingContext'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -23,11 +23,14 @@ interface ModelingSidebarProps {
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboardingStatus
const { context } = useModelingContext() const { openPanes, buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
openPanes: s.openPanes,
}))
const pointerEventsCssClass = const pointerEventsCssClass =
context.store?.buttonDownInStream || buttonDownInStream ||
onboardingStatus.current === 'camera' || onboardingStatus.current === 'camera' ||
context.store?.openPanes.length === 0 openPanes.length === 0
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto ' : 'pointer-events-auto '
@ -42,7 +45,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
maxWidth={800} maxWidth={800}
handleClasses={{ handleClasses={{
right: right:
(context.store?.openPanes.length === 0 ? 'hidden ' : 'block ') + (openPanes.length === 0 ? 'hidden ' : 'block ') +
'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ', 'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ',
left: 'hidden', left: 'hidden',
top: 'hidden', top: 'hidden',
@ -53,19 +56,15 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
bottomRight: 'hidden', bottomRight: 'hidden',
}} }}
> >
<div id="app-sidebar" className={styles.grid + ' flex-1'}> <div className={styles.grid + ' flex-1'}>
<ModelingSidebarSection id="sidebar-top" panes={topPanes} /> <ModelingSidebarSection panes={topPanes} />
<ModelingSidebarSection <ModelingSidebarSection panes={bottomPanes} alignButtons="end" />
id="sidebar-bottom"
panes={bottomPanes}
alignButtons="end"
/>
</div> </div>
</Resizable> </Resizable>
) )
} }
interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> { interface ModelingSidebarSectionProps {
panes: SidebarPane[] panes: SidebarPane[]
alignButtons?: 'start' | 'end' alignButtons?: 'start' | 'end'
} }
@ -73,16 +72,15 @@ interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> {
function ModelingSidebarSection({ function ModelingSidebarSection({
panes, panes,
alignButtons = 'start', alignButtons = 'start',
className,
...props
}: ModelingSidebarSectionProps) { }: ModelingSidebarSectionProps) {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const showDebugPanel = settings.context.modeling.showDebugPanel const showDebugPanel = settings.context.modeling.showDebugPanel
const paneIds = panes.map((pane) => pane.id) const paneIds = panes.map((pane) => pane.id)
const { send, context } = useModelingContext() const { openPanes, setOpenPanes } = useStore((s) => ({
const foundOpenPane = context.store?.openPanes.find((pane) => openPanes: s.openPanes,
paneIds.includes(pane) setOpenPanes: s.setOpenPanes,
) }))
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
const [currentPane, setCurrentPane] = useState( const [currentPane, setCurrentPane] = useState(
foundOpenPane || ('none' as SidebarType | 'none') foundOpenPane || ('none' as SidebarType | 'none')
) )
@ -90,37 +88,17 @@ function ModelingSidebarSection({
const togglePane = useCallback( const togglePane = useCallback(
(newPane: SidebarType | 'none') => { (newPane: SidebarType | 'none') => {
if (newPane === 'none') { if (newPane === 'none') {
send({ setOpenPanes(openPanes.filter((p) => p !== currentPane))
type: 'Set context',
data: {
openPanes: context.store?.openPanes.filter(
(p) => p !== currentPane
),
},
})
setCurrentPane('none') setCurrentPane('none')
} else if (newPane === currentPane) { } else if (newPane === currentPane) {
setCurrentPane('none') setCurrentPane('none')
send({ setOpenPanes(openPanes.filter((p) => p !== newPane))
type: 'Set context',
data: {
openPanes: context.store?.openPanes.filter((p) => p !== newPane),
},
})
} else { } else {
send({ setOpenPanes([...openPanes.filter((p) => p !== currentPane), newPane])
type: 'Set context',
data: {
openPanes: [
...context.store?.openPanes.filter((p) => p !== currentPane),
newPane,
],
},
})
setCurrentPane(newPane) setCurrentPane(newPane)
} }
}, },
[context.store?.openPanes, send, currentPane, setCurrentPane] [openPanes, setOpenPanes, currentPane, setCurrentPane]
) )
// Filter out the debug panel if it's not supposed to be shown // Filter out the debug panel if it's not supposed to be shown
@ -138,14 +116,14 @@ function ModelingSidebarSection({
if ( if (
!showDebugPanel.current && !showDebugPanel.current &&
currentPane === 'debug' && currentPane === 'debug' &&
context.store?.openPanes.includes('debug') openPanes.includes('debug')
) { ) {
togglePane('debug') togglePane('debug')
} }
}, [showDebugPanel.current, togglePane, context.store?.openPanes]) }, [showDebugPanel.current, togglePane, openPanes])
return ( return (
<div className={'group contents ' + className} {...props}> <div className="group contents">
<Tab.Group <Tab.Group
vertical vertical
selectedIndex={ selectedIndex={
@ -157,7 +135,6 @@ function ModelingSidebarSection({
}} }}
> >
<Tab.List <Tab.List
id={`${props.id}-ribbon`}
className={ className={
'pointer-events-auto ' + 'pointer-events-auto ' +
(alignButtons === 'start' (alignButtons === 'start'
@ -168,9 +145,7 @@ function ModelingSidebarSection({
: ' border-r-0') + : ' border-r-0') +
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' + ' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' + 'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
(context.store?.openPanes.length === 1 && currentPane === 'none' (openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
? 'pr-0.5'
: '')
} }
> >
<Tab key="none" className="sr-only"> <Tab key="none" className="sr-only">
@ -186,11 +161,10 @@ function ModelingSidebarSection({
))} ))}
</Tab.List> </Tab.List>
<Tab.Panels <Tab.Panels
id={`${props.id}-pane`}
as="article" as="article"
className={ className={
'col-start-2 col-span-1 ' + 'col-start-2 col-span-1 ' +
(context.store?.openPanes.length === 1 (openPanes.length === 1
? currentPane !== 'none' ? currentPane !== 'none'
? `row-start-1 row-end-3` ? `row-start-1 row-end-3`
: `hidden` : `hidden`

View File

@ -2,17 +2,22 @@ import { coreDump } from 'lang/wasm'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import React, { useMemo } from 'react' import React from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { useStore } from 'useStore'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
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 coreDumpManager = useMemo( const { htmlRef } = useStore((s) => ({
() => new CoreDumpManager(engineCommandManager, token), htmlRef: s.htmlRef,
[] }))
const coreDumpManager = new CoreDumpManager(
engineCommandManager,
htmlRef,
token
) )
async function refresh() { async function refresh() {

View File

@ -134,11 +134,6 @@ export const SettingsAuthProviderBase = ({
}, },
}) })
}, },
setEngineScaleGridVisibility: (context) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: (context) => { setClientTheme: (context) => {
const opposingTheme = getOppositeTheme(context.app.theme.current) const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme sceneInfra.theme = opposingTheme
@ -175,12 +170,7 @@ export const SettingsAuthProviderBase = ({
id: `${event.type}.success`, id: `${event.type}.success`,
}) })
}, },
'Execute AST': () => { 'Execute AST': () => kclManager.executeCode(true, true),
kclManager.isFirstRender = true
kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
}, },
services: { services: {
'Persist settings': (context) => 'Persist settings': (context) =>

View File

@ -1,4 +1,5 @@
import { MouseEventHandler, useEffect, useRef, useState } from 'react' import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils' import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -8,15 +9,26 @@ 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 = ({ className = '' }: { className?: string }) => {
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 {
mediaStream,
setButtonDownInStream,
didDragInStream,
setDidDragInStream,
streamDimensions,
} = useStore((s) => ({
mediaStream: s.mediaStream,
setButtonDownInStream: s.setButtonDownInStream,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
streamDimensions: s.streamDimensions,
}))
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext() const { state } = useModelingContext()
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const isNetworkOkay = const isNetworkOkay =
@ -55,10 +67,6 @@ export const Stream = () => {
}) })
}, []) }, [])
useEffect(() => {
setIsFirstRender(kclManager.isFirstRender)
}, [kclManager.isFirstRender])
useEffect(() => { useEffect(() => {
if ( if (
typeof window === 'undefined' || typeof window === 'undefined' ||
@ -66,16 +74,9 @@ export const Stream = () => {
) )
return return
if (!videoRef.current) return if (!videoRef.current) return
if (!context.store?.mediaStream) return if (!mediaStream) return
videoRef.current.srcObject = context.store.mediaStream videoRef.current.srcObject = mediaStream
}, [mediaStream])
send({
type: 'Set context',
data: {
videoElement: videoRef.current,
},
})
}, [context.store?.mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return if (!isNetworkOkay) return
@ -87,44 +88,25 @@ export const Stream = () => {
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,
el: videoRef.current, el: videoRef.current,
...context.store?.streamDimensions, ...streamDimensions,
}) })
send({ setButtonDownInStream(e.button)
type: 'Set context',
data: {
buttonDownInStream: e.button,
},
})
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return if (!isNetworkOkay) return
if (!videoRef.current) return if (!videoRef.current) return
send({ setButtonDownInStream(undefined)
type: 'Set context',
data: {
buttonDownInStream: undefined,
},
})
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
if (!context.store?.didDragInStream && butName(e).left) { if (!didDragInStream && butName(e).left) {
sendSelectEventToEngine( sendSelectEventToEngine(e, videoRef.current, streamDimensions)
e,
videoRef.current,
context.store?.streamDimensions
)
} }
send({ setDidDragInStream(false)
type: 'Set context',
data: {
didDragInStream: false,
},
})
setClickCoords(undefined) setClickCoords(undefined)
} }
@ -138,13 +120,8 @@ export const Stream = () => {
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) ** ((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
0.5 0.5
if (delta > 5 && !context.store?.didDragInStream) { if (delta > 5 && !didDragInStream) {
send({ setDidDragInStream(true)
type: 'Set context',
data: {
didDragInStream: true,
},
})
} }
} }
@ -179,14 +156,10 @@ export const Stream = () => {
</Loading> </Loading>
</div> </div>
)} )}
{(isLoading || isFirstRender) && ( {isLoading && (
<div className="text-center absolute inset-0"> <div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<Loading> <Loading>
{!isLoading && isFirstRender ? ( <span data-testid="loading-stream">Loading stream...</span>
<span data-testid="loading-stream">Building scene...</span>
) : (
<span data-testid="loading-stream">Loading stream...</span>
)}
</Loading> </Loading>
</div> </div>
)} )}

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { Program, Value, VariableDeclarator } from '../../lang/wasm' import { Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { Program, Value, VariableDeclarator } from '../../lang/wasm' import { Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { Program, ProgramMemory, Value } from '../../lang/wasm' import { Program, ProgramMemory, Value } from '../../lang/wasm'
import { import {

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm' import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
@ -145,7 +145,7 @@ export async function applyConstraintIntersect({
const { transforms, forcedSelectionRanges } = info const { transforms, forcedSelectionRanges } = info
const transform1 = transformSecondarySketchLinesTagFirst({ const transform1 = transformSecondarySketchLinesTagFirst({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges: forcedSelectionRanges, selectionRanges: forcedSelectionRanges,
transformInfos: transforms, transformInfos: transforms,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { PathToNode, Program, Value } from '../../lang/wasm' import { PathToNode, Program, Value } from '../../lang/wasm'
import { import {

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value } from '../../lang/wasm' import { BinaryPart, Program, Value } from '../../lang/wasm'
import { import {
@ -106,7 +106,7 @@ export async function applyConstraintAbsDistance({
const transformInfos = info.transforms const transformInfos = info.transforms
const transform1 = transformAstSketchLines({ const transform1 = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges: selectionRanges, selectionRanges: selectionRanges,
transformInfos, transformInfos,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
@ -128,7 +128,7 @@ export async function applyConstraintAbsDistance({
) )
const transform2 = transformAstSketchLines({ const transform2 = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges: selectionRanges, selectionRanges: selectionRanges,
transformInfos, transformInfos,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
@ -176,7 +176,7 @@ export function applyConstraintAxisAlign({
let finalValue = createIdentifier('ZERO') let finalValue = createIdentifier('ZERO')
return transformAstSketchLines({ return transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges: selectionRanges, selectionRanges: selectionRanges,
transformInfos, transformInfos,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm' import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
@ -100,7 +100,7 @@ export async function applyConstraintAngleBetween({
const transformInfos = info.transforms const transformInfos = info.transforms
const transformed1 = transformSecondarySketchLinesTagFirst({ const transformed1 = transformSecondarySketchLinesTagFirst({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges, selectionRanges,
transformInfos, transformInfos,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm' import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -108,7 +108,7 @@ export async function applyConstraintHorzVertDistance({
if (err(info)) return Promise.reject(info) if (err(info)) return Promise.reject(info)
const transformInfos = info.transforms const transformInfos = info.transforms
const transformed = transformSecondarySketchLinesTagFirst({ const transformed = transformSecondarySketchLinesTagFirst({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges, selectionRanges,
transformInfos, transformInfos,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,

View File

@ -1,4 +1,4 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value } from '../../lang/wasm' import { BinaryPart, Program, Value } from '../../lang/wasm'
import { import {
@ -84,7 +84,7 @@ export async function applyConstraintAngleLength({
const { transforms } = angleLength const { transforms } = angleLength
const sketched = transformAstSketchLines({ const sketched = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges, selectionRanges,
transformInfos: transforms, transformInfos: transforms,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
@ -139,7 +139,7 @@ export async function applyConstraintAngleLength({
} }
const retval = transformAstSketchLines({ const retval = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: kclManager.ast,
selectionRanges, selectionRanges,
transformInfos: transforms, transformInfos: transforms,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,

View File

@ -1,25 +1,16 @@
import { StateField, StateEffect, Annotation } from '@codemirror/state' import { StateField, StateEffect } from '@codemirror/state'
import { EditorView, Decoration } from '@codemirror/view' import { EditorView, Decoration } from '@codemirror/view'
export { EditorView } export { EditorView }
export const addLineHighlight = StateEffect.define<[number, number]>() export const addLineHighlight = StateEffect.define<[number, number]>()
const addLineHighlightAnnotation = Annotation.define<null>()
export const addLineHighlightEvent = addLineHighlightAnnotation.of(null)
export const lineHighlightField = StateField.define({ export const lineHighlightField = StateField.define({
create() { create() {
return Decoration.none return Decoration.none
}, },
update(lines, tr) { update(lines, tr) {
lines = lines.map(tr.changes) lines = lines.map(tr.changes)
const isLineHighlightEvent = tr.annotation(addLineHighlightEvent.type)
if (isLineHighlightEvent === undefined) {
return lines
}
const deco = [] const deco = []
for (let e of tr.effects) { for (let e of tr.effects) {
if (e.is(addLineHighlight)) { if (e.is(addLineHighlight)) {

View File

@ -1,25 +1,13 @@
import { hasNextSnippetField } from '@codemirror/autocomplete'
import { EditorView, ViewUpdate } from '@codemirror/view' import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, Annotation, Transaction } from '@codemirror/state' import { EditorSelection, SelectionRange } from '@codemirror/state'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager, sceneInfra } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections' import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands' import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight, addLineHighlightEvent } from './highlightextension' import { addLineHighlight } from './highlightextension'
import { import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
forEachDiagnostic,
Diagnostic,
setDiagnosticsEffect,
} from '@codemirror/lint'
const updateOutsideEditorAnnotation = Annotation.define<null>()
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(null)
const modelingMachineAnnotation = Annotation.define<null>()
export const modelingMachineEvent = modelingMachineAnnotation.of(null)
const setDiagnosticsAnnotation = Annotation.define<null>()
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(null)
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean { function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
@ -27,7 +15,6 @@ function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
export default class EditorManager { export default class EditorManager {
private _editorView: EditorView | null = null private _editorView: EditorView | null = null
private _copilotEnabled: boolean = true
private _isShiftDown: boolean = false private _isShiftDown: boolean = false
private _selectionRanges: Selections = { private _selectionRanges: Selections = {
@ -35,6 +22,8 @@ export default class EditorManager {
codeBasedSelections: [], codeBasedSelections: [],
} }
private _lastSelectionEvent: number | null = null
lastSelection: string = ''
private _lastEvent: { event: string; time: number } | null = null private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {} private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
@ -48,14 +37,6 @@ export default class EditorManager {
private _highlightRange: [number, number] = [0, 0] private _highlightRange: [number, number] = [0, 0]
setCopilotEnabled(enabled: boolean) {
this._copilotEnabled = enabled
}
get copilotEnabled(): boolean {
return this._copilotEnabled
}
setEditorView(editorView: EditorView) { setEditorView(editorView: EditorView) {
this._editorView = editorView this._editorView = editorView
} }
@ -76,6 +57,10 @@ export default class EditorManager {
this._selectionRanges = selectionRanges this._selectionRanges = selectionRanges
} }
set lastSelectionEvent(time: number) {
this._lastSelectionEvent = time
}
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) { set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
this._modelingSend = send this._modelingSend = send
} }
@ -98,39 +83,32 @@ export default class EditorManager {
setHighlightRange(selection: Selection['range']): void { setHighlightRange(selection: Selection['range']): void {
this._highlightRange = selection this._highlightRange = selection
const editorView = this.editorView
const safeEnd = Math.min( const safeEnd = Math.min(
selection[1], selection[1],
this._editorView?.state.doc.length || selection[1] editorView?.state.doc.length || selection[1]
) )
if (this._editorView) { if (editorView) {
this._editorView.dispatch({ editorView.dispatch({
effects: addLineHighlight.of([selection[0], safeEnd]), effects: addLineHighlight.of([selection[0], safeEnd]),
annotations: [
updateOutsideEditorEvent,
addLineHighlightEvent,
Transaction.addToHistory.of(false),
],
}) })
} }
} }
clearDiagnostics(): void { clearDiagnostics(): void {
this.setDiagnostics([]) if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
} }
setDiagnostics(diagnostics: Diagnostic[]): void { setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this._editorView) return if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
this._editorView.dispatch({
effects: [setDiagnosticsEffect.of(diagnostics)],
annotations: [setDiagnosticsEvent, Transaction.addToHistory.of(false)],
})
} }
addDiagnostics(diagnostics: Diagnostic[]): void { addDiagnostics(diagnostics: Diagnostic[]): void {
if (!this._editorView) return if (!this.editorView) return
forEachDiagnostic(this._editorView.state, function (diag) { forEachDiagnostic(this.editorView.state, function (diag) {
diagnostics.push(diag) diagnostics.push(diag)
}) })
@ -144,7 +122,9 @@ export default class EditorManager {
uniqueDiagnostics.add(diagnostic) uniqueDiagnostics.add(diagnostic)
}) })
this.setDiagnostics([...uniqueDiagnostics]) this.editorView.dispatch(
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
)
} }
undo() { undo() {
@ -194,35 +174,56 @@ export default class EditorManager {
].range[1] ].range[1]
) )
) )
if (!this.editorView) {
if (!this._editorView) {
return return
} }
this.editorView.dispatch({
this._editorView.dispatch({
selection: EditorSelection.create(codeBasedSelections, 1), selection: EditorSelection.create(codeBasedSelections, 1),
annotations: [
updateOutsideEditorEvent,
Transaction.addToHistory.of(false),
],
}) })
} }
// We will ONLY get here if the user called a select event.
// This is handled by the code mirror kcl plugin.
// If you call this function from somewhere else, you best know wtf you are
// doing. (jess)
handleOnViewUpdate(viewUpdate: ViewUpdate): void { handleOnViewUpdate(viewUpdate: ViewUpdate): void {
if (!this._editorView) { // If we are just fucking around in a snippet, return early and don't
this.setEditorView(viewUpdate.view) // trigger stuff below that might cause the component to re-render.
} // Otherwise we will not be able to tab thru the snippet portions.
// We explicitly dont check HasPrevSnippetField because we always add
const ranges = viewUpdate?.state?.selection?.ranges || [] // a ${} to the end of the function so that's fine.
if (ranges.length === 0) { if (hasNextSnippetField(viewUpdate.view.state)) {
return return
} }
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool'] if (this.editorView === null) {
this.setEditorView(viewUpdate.view)
}
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === this.lastSelection) {
// onUpdate is noisy and is fired a lot by extensions
// since we're only interested in selections changes we can ignore most of these.
return
}
// note this is also set from the "Set selection" action to stop code mirror from updating selections right after
// selections are made from the scene
this.lastSelection = selString
if (
this._lastSelectionEvent &&
Date.now() - this._lastSelectionEvent < 150
) {
return // update triggered by scene selection
}
if (sceneInfra.selected) {
return // mid drag
}
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
'Equip rectangle tool',
]
if (!this._modelingEvent) { if (!this._modelingEvent) {
return return
@ -265,3 +266,7 @@ export default class EditorManager {
) )
} }
} }
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -6,6 +6,7 @@ import {
unregisterServerCapability, unregisterServerCapability,
} from './server-capability-registration' } from './server-capability-registration'
import { Codec, FromServer, IntoServer } from './codec' import { Codec, FromServer, IntoServer } from './codec'
import { err } from 'lib/trap'
const client_capabilities: LSP.ClientCapabilities = { const client_capabilities: LSP.ClientCapabilities = {
textDocument: { textDocument: {
@ -66,13 +67,8 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
#fromServer: FromServer #fromServer: FromServer
private serverCapabilities: LSP.ServerCapabilities<any> = {} private serverCapabilities: LSP.ServerCapabilities<any> = {}
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
private initializedCallback: () => void
constructor( constructor(fromServer: FromServer, intoServer: IntoServer) {
fromServer: FromServer,
intoServer: IntoServer,
initializedCallback: () => void
) {
super( super(
new jsrpc.JSONRPCServer(), new jsrpc.JSONRPCServer(),
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => { new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
@ -86,7 +82,6 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
}) })
) )
this.#fromServer = fromServer this.#fromServer = fromServer
this.initializedCallback = initializedCallback
} }
async start(): Promise<void> { async start(): Promise<void> {
@ -129,9 +124,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
this.serverCapabilities, this.serverCapabilities,
capabilityRegistration capabilityRegistration
) )
if (caps instanceof Error) { if (err(caps)) return (this.serverCapabilities = {})
return (this.serverCapabilities = {})
}
this.serverCapabilities = caps this.serverCapabilities = caps
} }
) )
@ -146,9 +139,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
this.serverCapabilities, this.serverCapabilities,
capabilityUnregistration capabilityUnregistration
) )
if (caps instanceof Error) { if (err(caps)) return (this.serverCapabilities = {})
return (this.serverCapabilities = {})
}
this.serverCapabilities = caps this.serverCapabilities = caps
} }
) )
@ -160,7 +151,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
{ {
processId: null, processId: null,
clientInfo: { clientInfo: {
name: 'codemirror-lsp-client', name: 'kcl-language-client',
}, },
capabilities: client_capabilities, capabilities: client_capabilities,
rootUri: null, rootUri: null,
@ -172,8 +163,6 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
// notify "initialized": client --> server // notify "initialized": client --> server
this.notify(LSP.InitializedNotification.type.method, {}) this.notify(LSP.InitializedNotification.type.method, {})
this.initializedCallback()
await Promise.all( await Promise.all(
this.afterInitializedHooks.map((f: () => Promise<void>) => f()) this.afterInitializedHooks.map((f: () => Promise<void>) => f())
) )

View File

@ -1,16 +1,12 @@
import * as jsrpc from 'json-rpc-2.0' import * as jsrpc from 'json-rpc-2.0'
import * as vsrpc from 'vscode-jsonrpc' import * as vsrpc from 'vscode-jsonrpc'
import Bytes from './bytes' import Bytes from './codec/bytes'
import StreamDemuxer from './demuxer' import StreamDemuxer from './codec/demuxer'
import Headers from './headers' import Headers from './codec/headers'
import Queue from './queue' import Queue from './codec/queue'
import Tracer from './tracer' import Tracer from './tracer'
import { LspWorkerEventType, LspWorker } from './types'
export enum LspWorkerEventType {
Init = 'init',
Call = 'call',
}
export const encoder = new TextEncoder() export const encoder = new TextEncoder()
export const decoder = new TextDecoder() export const decoder = new TextDecoder()
@ -37,24 +33,16 @@ export class IntoServer
implements AsyncGenerator<Uint8Array, never, void> implements AsyncGenerator<Uint8Array, never, void>
{ {
private worker: Worker | null = null private worker: Worker | null = null
private type_: String | null = null private type_: LspWorker | null = null
constructor(type_?: LspWorker, worker?: Worker) {
private trace: boolean = false
constructor(type_?: String, worker?: Worker, trace?: boolean) {
super() super()
if (worker && type_) { if (worker && type_) {
this.worker = worker this.worker = worker
this.type_ = type_ this.type_ = type_
} }
this.trace = trace || false
} }
enqueue(item: Uint8Array): void { enqueue(item: Uint8Array): void {
if (this.trace) { Tracer.client(Headers.remove(decoder.decode(item)))
Tracer.client(Headers.remove(decoder.decode(item)))
}
if (this.worker) { if (this.worker) {
this.worker.postMessage({ this.worker.postMessage({
worker: this.type_, worker: this.type_,
@ -83,7 +71,7 @@ export namespace FromServer {
// Calls private method .start() which can throw. // Calls private method .start() which can throw.
// This is an odd one of the bunch but try/catch seems most suitable here. // This is an odd one of the bunch but try/catch seems most suitable here.
try { try {
return new StreamDemuxer(false) return new StreamDemuxer()
} catch (e: any) { } catch (e: any) {
return e return e
} }

View File

@ -1,10 +1,10 @@
import * as vsrpc from 'vscode-jsonrpc' import * as vsrpc from 'vscode-jsonrpc'
import { Codec } from '.'
import Bytes from './bytes' import Bytes from './bytes'
import Queue from './queue'
import Tracer from './tracer'
import PromiseMap from './map' import PromiseMap from './map'
import Queue from './queue'
import Tracer from '../tracer'
import { Codec } from '../codec'
export default class StreamDemuxer extends Queue<Uint8Array> { export default class StreamDemuxer extends Queue<Uint8Array> {
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> = readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
@ -15,12 +15,9 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
new Queue<vsrpc.RequestMessage>() new Queue<vsrpc.RequestMessage>()
readonly #start: Promise<void> readonly #start: Promise<void>
private trace: boolean = false
constructor(trace?: boolean) { constructor() {
super() super()
this.trace = trace || false
this.#start = this.start() this.#start = this.start()
} }
@ -67,10 +64,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
contentLength = null contentLength = null
const message = JSON.parse(delimited) as vsrpc.Message const message = JSON.parse(delimited) as vsrpc.Message
Tracer.server(message)
if (this.trace) {
Tracer.server(message)
}
// demux the message stream // demux the message stream
if (vsrpc.Message.isResponse(message) && null != message.id) { if (vsrpc.Message.isResponse(message) && null != message.id) {
@ -91,9 +85,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
add(bytes: Uint8Array): void { add(bytes: Uint8Array): void {
const message = Codec.decode(bytes) as vsrpc.Message const message = Codec.decode(bytes) as vsrpc.Message
if (this.trace) { Tracer.server(message)
Tracer.server(message)
}
// demux the message stream // demux the message stream
if (vsrpc.Message.isResponse(message) && null != message.id) { if (vsrpc.Message.isResponse(message) && null != message.id) {

View File

@ -1,16 +1,11 @@
/// Thanks to the Cursor folks for their heavy lifting here. /// Thanks to the Cursor folks for their heavy lifting here.
/// This has been heavily modified from their original implementation but we are
/// still grateful.
import { indentUnit } from '@codemirror/language' import { indentUnit } from '@codemirror/language'
import { import {
Decoration, Decoration,
DecorationSet, DecorationSet,
EditorView, EditorView,
KeyBinding,
PluginValue,
ViewPlugin, ViewPlugin,
ViewUpdate, ViewUpdate,
keymap,
} from '@codemirror/view' } from '@codemirror/view'
import { import {
Annotation, Annotation,
@ -22,37 +17,17 @@ import {
Transaction, Transaction,
} from '@codemirror/state' } from '@codemirror/state'
import { completionStatus } from '@codemirror/autocomplete' import { completionStatus } from '@codemirror/autocomplete'
import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
import { import {
offsetToPos, LanguageServerPlugin,
posToOffset, documentUri,
LanguageServerOptions,
LanguageServerClient,
docPathFacet,
languageId, languageId,
} from '@kittycad/codemirror-lsp-client' workspaceFolders,
import { deferExecution } from 'lib/utils' } from 'editor/plugins/lsp/plugin'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
import { editorManager } from 'lib/singletons'
const copilotPluginAnnotation = Annotation.define<null>()
export const copilotPluginEvent = copilotPluginAnnotation.of(null)
const rejectSuggestionAnnotation = Annotation.define<null>()
export const rejectSuggestionCommand = rejectSuggestionAnnotation.of(null)
// Effects to tell StateEffect what to do with GhostText
const addSuggestion = StateEffect.define<Suggestion>()
const acceptSuggestion = StateEffect.define<null>()
const clearSuggestion = StateEffect.define<null>()
const typeFirst = StateEffect.define<number>()
const ghostMark = Decoration.mark({ class: 'cm-ghostText' }) const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
const changesDelay = 600
interface Suggestion { interface Suggestion {
text: string text: string
displayText: string displayText: string
@ -63,10 +38,15 @@ interface Suggestion {
uuid: string uuid: string
} }
// Effects to tell StateEffect what to do with GhostText
const addSuggestion = StateEffect.define<Suggestion>()
const acceptSuggestion = StateEffect.define<null>()
const clearSuggestion = StateEffect.define<null>()
const typeFirst = StateEffect.define<number>()
interface CompletionState { interface CompletionState {
ghostText: GhostText | null ghostText: GhostText | null
} }
interface GhostText { interface GhostText {
text: string text: string
displayText: string displayText: string
@ -80,19 +60,11 @@ interface GhostText {
uuid: string uuid: string
} }
const completionDecoration = StateField.define<CompletionState>({ export const completionDecoration = StateField.define<CompletionState>({
create(_state: EditorState) { create(_state: EditorState) {
return { ghostText: null } return { ghostText: null }
}, },
update(state: CompletionState, transaction: Transaction) { update(state: CompletionState, transaction: Transaction) {
// We only care about events from this plugin.
if (
transaction.annotation(copilotPluginEvent.type) === undefined &&
transaction.annotation(rejectSuggestionCommand.type) === undefined
) {
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
@ -188,448 +160,331 @@ const completionDecoration = StateField.define<CompletionState>({
), ),
}) })
// A view plugin that requests completions from the server after a delay const copilotEvent = Annotation.define<null>()
export class CompletionRequester implements PluginValue {
private client: LanguageServerClient
private lastPos: number = 0
private queuedUids: string[] = [] /****************************************************************************
************************* COMMANDS ******************************************
*****************************************************************************/
private _deffererCodeUpdate = deferExecution(() => { const acceptSuggestionCommand = (
this.requestCompletions() copilotClient: LanguageServerClient,
}, changesDelay) view: EditorView
) => {
private _deffererUserSelect = deferExecution(() => { // We delete the ghost text and insert the suggestion.
this.rejectSuggestionCommand() // We also set the cursor to the end of the suggestion.
}, changesDelay) const ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client
}
update(viewUpdate: ViewUpdate) {
// Make sure we are in a state where we can request completions.
if (!editorManager.copilotEnabled) {
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(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 {
return this.view.state.field(completionDecoration)?.ghostText || null
}
containsGhostText(): boolean {
return this.ghostText() !== null
}
autocompleting(): boolean {
return completionStatus(this.view.state) === 'active'
}
notFocused(): boolean {
return !this.view.hasFocus
}
async requestCompletions(): Promise<void> {
if (
this.containsGhostText() ||
this.autocompleting() ||
this.notFocused()
) {
return
}
const pos = this.view.state.selection.main.head
// Check if the position has changed
if (pos !== this.lastPos) {
return
}
// Get the current position and source
const state = this.view.state
const dUri = state.facet(docPathFacet)
// Request completion from the server
const completionResult = await this.getCompletion({
doc: {
source: state.doc.toString(),
tabSize: state.facet(EditorState.tabSize),
indentSize: 1,
insertSpaces: true,
path: dUri.split('/').pop()!,
uri: dUri,
relativePath: dUri.replace('file://', ''),
languageId: state.facet(languageId),
position: offsetToPos(state.doc, pos),
},
})
if (completionResult.completions.length === 0) {
return
}
let {
text,
displayText,
range: { start },
position,
uuid,
} = completionResult.completions[0]
if (text.length === 0 || displayText.length === 0) {
return
}
const startPos = posToOffset(state.doc, {
line: start.line,
character: start.character,
})
if (startPos === undefined) {
return
}
const endGhostOffset = posToOffset(state.doc, {
line: position.line,
character: position.character,
})
if (endGhostOffset === undefined) {
return
}
const endGhostPos = endGhostOffset + displayText.length
// EndPos is the position that marks the complete end
// of what is to be replaced when we accept a completion
// result
const endPos = startPos + text.length
// Check if they changed position.
if (pos !== this.lastPos) {
return
}
// Make sure we are not currently completing.
if (this.autocompleting() || this.notFocused()) {
return
}
// 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.view.state.doc.lineAt(pos)
if (line.to !== pos) {
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.view.dispatch({
changes: {
from: pos,
to: line.to,
insert: '',
},
selection: { anchor: pos },
effects: typeFirst.of(ending.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
}
}
this.view.dispatch({
changes: {
from: pos,
to: pos,
insert: displayText,
},
effects: [
addSuggestion.of({
displayText,
endReplacement: endGhostPos,
text,
cursorPos: pos,
startPos,
endPos,
uuid,
}),
],
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
this.lastPos = pos
return
}
acceptSuggestionCommand(): boolean {
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
// We delete the ghost text and insert the suggestion.
// We also set the cursor to the end of the suggestion.
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
const actualTextStart = ghostText.startPos
const actualTextEnd = ghostText.endPos
const replacementEnd = ghostText.endReplacement
const suggestion = ghostText.text
this.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: acceptSuggestion.of(null),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
this.view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
insert: suggestion,
},
selection: { anchor: actualTextEnd },
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
})
this.accept(ghostText.uuid)
return true
}
rejectSuggestionCommand(): boolean {
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
// We delete the suggestion, then carry through with the original keypress
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
this.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: clearSuggestion.of(null),
annotations: [
rejectSuggestionCommand,
Transaction.addToHistory.of(false),
],
})
this.reject()
return false return false
} }
sameKeyCommand(key: string) { const ghostTextStart = ghostText.displayPos
const ghostText = this.ghostText() const ghostTextEnd = ghostText.endGhostText
if (!ghostText) {
return false
}
const tabKey = 'Tab' const actualTextStart = ghostText.startPos
const actualTextEnd = ghostText.endPos
// 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 replacementEnd = ghostText.endReplacement
const ghostTextStart = ghostText.displayPos
const indent = this.view.state.facet(indentUnit)
if (key === tabKey && ghostText.displayText.startsWith(indent)) { const suggestion = ghostText.text
this.view.dispatch({
selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
return true
} else if (key === tabKey) {
return this.acceptSuggestionCommand()
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
return this.rejectSuggestionCommand()
} else if (ghostText.displayText.length === 1) {
return this.acceptSuggestionCommand()
} else {
// Use this to delete the first letter of the suggestion
this.view.dispatch({
selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
return true view.dispatch({
} changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
// selection: {anchor: actualTextEnd},
effects: acceptSuggestion.of(null),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
insert: suggestion,
},
selection: { anchor: actualTextEnd },
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)],
})
copilotClient.accept(ghostText.uuid)
return true
}
export const rejectSuggestionCommand = (
copilotClient: LanguageServerClient,
view: EditorView
) => {
// We delete the suggestion, then carry through with the original keypress
const ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
return false
} }
async getCompletion( const ghostTextStart = ghostText.displayPos
params: CopilotLspCompletionParams const ghostTextEnd = ghostText.endGhostText
): Promise<CopilotCompletionResponse> {
const response: CopilotCompletionResponse = await this.client.requestCustom(
'copilot/getCompletions',
params
)
//
this.queuedUids = [...response.completions.map((c) => c.uuid)]
return response
}
async accept(uuid: string) { view.dispatch({
const badUids = this.queuedUids.filter((u) => u !== uuid) changes: {
this.queuedUids = [] from: ghostTextStart,
this.acceptCompletion({ uuid }) to: ghostTextEnd,
this.rejectCompletions({ uuids: badUids }) insert: '',
} },
effects: clearSuggestion.of(null),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
async reject() { copilotClient.reject()
const badUids = this.queuedUids return false
this.queuedUids = [] }
this.rejectCompletions({ uuids: badUids })
}
acceptCompletion(params: CopilotAcceptCompletionParams) { const sameKeyCommand = (
this.client.notifyCustom('copilot/notifyAccepted', params) copilotClient: LanguageServerClient,
view: EditorView,
key: string
) => {
// 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 ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
return false
} }
const ghostTextStart = ghostText.displayPos
const indent = view.state.facet(indentUnit)
rejectCompletions(params: CopilotRejectCompletionParams) { if (key === 'Tab' && ghostText.displayText.startsWith(indent)) {
this.client.notifyCustom('copilot/notifyRejected', params) view.dispatch({
selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
return true
} else if (key === 'Tab') {
return acceptSuggestionCommand(copilotClient, view)
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
return rejectSuggestionCommand(copilotClient, view)
} else if (ghostText.displayText.length === 1) {
return acceptSuggestionCommand(copilotClient, view)
} else {
// Use this to delete the first letter of the suggestion
view.dispatch({
selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
return true
} }
} }
export const copilotPlugin = (options: LanguageServerOptions): Extension => { const completionPlugin = (copilotClient: LanguageServerClient) =>
let plugin: CompletionRequester | null = null EditorView.domEventHandlers({
const completionPlugin = ViewPlugin.define(
(view) => (plugin = new CompletionRequester(view, options.client))
)
const domHandlers = EditorView.domEventHandlers({
keydown(event, view) { keydown(event, view) {
if ( if (
event.key !== 'Shift' && event.key !== 'Shift' &&
event.key !== 'Control' && event.key !== 'Control' &&
event.key !== 'Alt' && event.key !== 'Alt' &&
event.key !== 'Backspace' &&
event.key !== 'Delete' &&
event.key !== 'Meta' event.key !== 'Meta'
) { ) {
if (view.plugin === null) return false return sameKeyCommand(copilotClient, view, event.key)
// Get the current plugin from the map.
const p = view.plugin(completionPlugin)
if (p === null) return false
return p.sameKeyCommand(event.key)
} else { } else {
return false return false
} }
}, },
mousedown(event, view) {
return rejectSuggestionCommand(copilotClient, view)
},
}) })
const rejectSuggestionCommand = (view: EditorView): boolean => { const viewCompletionPlugin = (copilotClient: LanguageServerClient) =>
// Get the current plugin from the map. EditorView.updateListener.of((update) => {
const p = view.plugin(completionPlugin) if (update.focusChanged) {
if (p === null) return false rejectSuggestionCommand(copilotClient, update.view)
}
})
// A view plugin that requests completions from the server after a delay
const completionRequester = (client: LanguageServerClient) => {
let timeout: any = null
let lastPos = 0
return p.rejectSuggestionCommand() const badUpdate = (update: ViewUpdate) => {
for (const tr of update.transactions) {
if (tr.annotation(copilotEvent) !== undefined) {
return true
}
}
return false
}
const containsGhostText = (update: ViewUpdate) => {
return update.state.field(completionDecoration).ghostText != null
}
const autocompleting = (update: ViewUpdate) => {
return completionStatus(update.state) === 'active'
}
const notFocused = (update: ViewUpdate) => {
return !update.view.hasFocus
} }
const copilotAutocompleteKeymap: readonly KeyBinding[] = [ return EditorView.updateListener.of((update: ViewUpdate) => {
{ if (
key: 'Tab', update.docChanged &&
run: (view: EditorView): boolean => { !update.transactions.some((tr) =>
if (view.plugin === null) return false tr.effects.some((e) => e.is(acceptSuggestion) || e.is(clearSuggestion))
)
) {
// Cancel the previous timeout
if (timeout) {
clearTimeout(timeout)
}
if (
badUpdate(update) ||
containsGhostText(update) ||
autocompleting(update) ||
notFocused(update)
) {
return
}
// Get the current plugin from the map. // Get the current position and source
const p = view.plugin(completionPlugin) const state = update.state
if (p === null) return false const pos = state.selection.main.head
const source = state.doc.toString()
return p.sameKeyCommand('Tab') const dUri = state.facet(documentUri)
}, const path = dUri.split('/').pop()!
}, const relativePath = dUri.replace('file://', '')
{
key: 'Backspace',
run: rejectSuggestionCommand,
},
{
key: 'Delete',
run: rejectSuggestionCommand,
},
{ key: 'Mod-z', run: rejectSuggestionCommand, preventDefault: true },
{
key: 'Mod-y',
mac: 'Mod-Shift-z',
run: rejectSuggestionCommand,
preventDefault: true,
},
{
linux: 'Ctrl-Shift-z',
run: rejectSuggestionCommand,
preventDefault: true,
},
{ key: 'Mod-u', run: rejectSuggestionCommand, preventDefault: true },
{
key: 'Alt-u',
mac: 'Mod-Shift-u',
run: rejectSuggestionCommand,
preventDefault: true,
},
]
const copilotAutocompleteKeymapExt = Prec.highest( // Set a new timeout to request completion
keymap.computeN([], () => [copilotAutocompleteKeymap]) timeout = setTimeout(async () => {
) // Check if the position has changed
if (pos === lastPos) {
// Request completion from the server
try {
const completionResult = await client.getCompletion({
doc: {
source,
tabSize: state.facet(EditorState.tabSize),
indentSize: 1,
insertSpaces: true,
path,
uri: dUri,
relativePath,
languageId: state.facet(languageId),
position: offsetToPos(state.doc, pos),
},
})
if (completionResult.completions.length === 0) {
return
}
let {
text,
displayText,
range: { start },
position,
uuid,
} = completionResult.completions[0]
const startPos = posToOffset(state.doc, {
line: start.line,
character: start.character,
})!
const endGhostPos =
posToOffset(state.doc, {
line: position.line,
character: position.character,
})! + displayText.length
// EndPos is the position that marks the complete end
// of what is to be replaced when we accept a completion
// result
const endPos = startPos + text.length
// Check if the position is still the same
if (
pos === lastPos &&
completionStatus(update.view.state) !== 'active' &&
update.view.hasFocus
) {
// 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 = update.view.state.doc.lineAt(pos)
if (line.to !== pos) {
const ending = update.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
update.view.dispatch({
changes: {
from: pos,
to: line.to,
insert: '',
},
selection: { anchor: pos },
effects: typeFirst.of(ending.length),
annotations: [
copilotEvent.of(null),
Transaction.addToHistory.of(false),
],
})
}
}
update.view.dispatch({
changes: {
from: pos,
to: pos,
insert: displayText,
},
effects: [
addSuggestion.of({
displayText,
endReplacement: endGhostPos,
text,
cursorPos: pos,
startPos,
endPos,
uuid,
}),
],
annotations: [
copilotEvent.of(null),
Transaction.addToHistory.of(false),
],
})
}
} catch (error) {
console.warn('copilot completion failed', error)
// Javascript wait for 500ms for some reason is necessary here.
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG
await new Promise((resolve) => setTimeout(resolve, 300))
}
}
}, 150)
// Update the last position
lastPos = pos
}
})
}
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
return [ return [
completionPlugin, documentUri.of(options.documentUri),
copilotAutocompleteKeymapExt, languageId.of('kcl'),
domHandlers, workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
),
completionDecoration, completionDecoration,
EditorView.focusChangeEffect.of((_, focusing) => { Prec.highest(completionPlugin(options.client)),
if (plugin === null) return null Prec.highest(viewCompletionPlugin(options.client)),
completionRequester(options.client),
plugin.rejectSuggestionCommand()
return null
}),
] ]
} }

View File

@ -1,8 +1,16 @@
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client'
import { FromServer, IntoServer } from './codec' import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
import Client from './jsonrpc' import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
import { LanguageServerPlugin } from '../plugin/lsp' import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams'
import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams'
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
import { LspWorker } from './types'
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/ // https://microsoft.github.io/language-server-protocol/specifications/specification-current/
@ -23,6 +31,12 @@ interface LSPRequestMap {
LSP.TextEdit[] | null LSP.TextEdit[] | null
] ]
'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]] 'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]]
'copilot/getCompletions': [
CopilotLspCompletionParams,
CopilotCompletionResponse
]
'kcl/updateUnits': [UpdateUnitsParams, UpdateUnitsResponse | null]
'kcl/updateCanExecute': [UpdateCanExecuteParams, UpdateCanExecuteResponse]
} }
// Client to server // Client to server
@ -35,13 +49,21 @@ interface LSPNotifyMap {
'workspace/didCreateFiles': LSP.CreateFilesParams 'workspace/didCreateFiles': LSP.CreateFilesParams
'workspace/didRenameFiles': LSP.RenameFilesParams 'workspace/didRenameFiles': LSP.RenameFilesParams
'workspace/didDeleteFiles': LSP.DeleteFilesParams 'workspace/didDeleteFiles': LSP.DeleteFilesParams
'copilot/notifyAccepted': CopilotAcceptCompletionParams
'copilot/notifyRejected': CopilotRejectCompletionParams
} }
export interface LanguageServerClientOptions { export interface LanguageServerClientOptions {
name: string client: Client
fromServer: FromServer name: LspWorker
intoServer: IntoServer }
initializedCallback: () => void
export interface LanguageServerOptions {
// We assume this is the main project directory, we are currently working in.
workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
} }
export class LanguageServerClient { export class LanguageServerClient {
@ -54,18 +76,18 @@ export class LanguageServerClient {
public initializePromise: Promise<void> public initializePromise: Promise<void>
constructor(options: LanguageServerClientOptions) { private isUpdatingSemanticTokens: boolean = false
this.name = options.name private semanticTokens: SemanticToken[] = []
this.plugins = [] private queuedUids: string[] = []
this.client = new Client( constructor(options: LanguageServerClientOptions) {
options.fromServer, this.plugins = []
options.intoServer, this.client = options.client
options.initializedCallback this.name = options.name
)
this.ready = false this.ready = false
this.queuedUids = []
this.initializePromise = this.initialize() this.initializePromise = this.initialize()
} }
@ -89,10 +111,19 @@ export class LanguageServerClient {
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
this.notify('textDocument/didOpen', params) this.notify('textDocument/didOpen', params)
// Update the facet of the plugins to the correct value.
for (const plugin of this.plugins) {
plugin.documentUri = params.textDocument.uri
plugin.languageId = params.textDocument.languageId
}
this.updateSemanticTokens(params.textDocument.uri)
} }
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) { textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
this.notify('textDocument/didChange', params) this.notify('textDocument/didChange', params)
this.updateSemanticTokens(params.textDocument.uri)
} }
textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) { textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) {
@ -103,9 +134,18 @@ export class LanguageServerClient {
added: LSP.WorkspaceFolder[], added: LSP.WorkspaceFolder[],
removed: LSP.WorkspaceFolder[] removed: LSP.WorkspaceFolder[]
) { ) {
// Add all the current workspace folders in the plugin to removed.
for (const plugin of this.plugins) {
removed.push(...plugin.workspaceFolders)
}
this.notify('workspace/didChangeWorkspaceFolders', { this.notify('workspace/didChangeWorkspaceFolders', {
event: { added, removed }, event: { added, removed },
}) })
// Add all the new workspace folders to the plugins.
for (const plugin of this.plugins) {
plugin.workspaceFolders = added
}
} }
workspaceDidCreateFiles(params: LSP.CreateFilesParams) { workspaceDidCreateFiles(params: LSP.CreateFilesParams) {
@ -120,13 +160,33 @@ export class LanguageServerClient {
this.notify('workspace/didDeleteFiles', params) this.notify('workspace/didDeleteFiles', params)
} }
async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) { async updateSemanticTokens(uri: string) {
const serverCapabilities = this.getServerCapabilities() const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) { if (!serverCapabilities.semanticTokensProvider) {
return return
} }
return this.request('textDocument/semanticTokens/full', params) // Make sure we can only run, if we aren't already running.
if (!this.isUpdatingSemanticTokens) {
this.isUpdatingSemanticTokens = true
const result = await this.request('textDocument/semanticTokens/full', {
textDocument: {
uri,
},
})
this.semanticTokens = await deserializeTokens(
result.data,
this.getServerCapabilities().semanticTokensProvider
)
this.isUpdatingSemanticTokens = false
}
}
getSemanticTokens(): SemanticToken[] {
return this.semanticTokens
} }
async textDocumentHover(params: LSP.HoverParams) { async textDocumentHover(params: LSP.HoverParams) {
@ -179,10 +239,6 @@ export class LanguageServerClient {
return this.client.request(method, params) as Promise<LSPRequestMap[K][1]> return this.client.request(method, params) as Promise<LSPRequestMap[K][1]>
} }
requestCustom<P, R>(method: string, params: P): Promise<R> {
return this.client.request(method, params) as Promise<R>
}
private notify<K extends keyof LSPNotifyMap>( private notify<K extends keyof LSPNotifyMap>(
method: K, method: K,
params: LSPNotifyMap[K] params: LSPNotifyMap[K]
@ -190,8 +246,44 @@ export class LanguageServerClient {
return this.client.notify(method, params) return this.client.notify(method, params)
} }
notifyCustom<P>(method: string, params: P): void { async getCompletion(params: CopilotLspCompletionParams) {
return this.client.notify(method, params) const response = await this.request('copilot/getCompletions', params)
//
this.queuedUids = [...response.completions.map((c) => c.uuid)]
return response
}
async accept(uuid: string) {
const badUids = this.queuedUids.filter((u) => u !== uuid)
this.queuedUids = []
this.acceptCompletion({ uuid })
this.rejectCompletions({ uuids: badUids })
}
async reject() {
const badUids = this.queuedUids
this.queuedUids = []
this.rejectCompletions({ uuids: badUids })
}
acceptCompletion(params: CopilotAcceptCompletionParams) {
this.notify('copilot/notifyAccepted', params)
}
rejectCompletions(params: CopilotRejectCompletionParams) {
this.notify('copilot/notifyRejected', params)
}
async updateUnits(
params: UpdateUnitsParams
): Promise<UpdateUnitsResponse | null> {
return await this.request('kcl/updateUnits', params)
}
async updateCanExecute(
params: UpdateCanExecuteParams
): Promise<UpdateCanExecuteResponse> {
return await this.request('kcl/updateCanExecute', params)
} }
private processNotifications(notification: LSP.NotificationMessage) { private processNotifications(notification: LSP.NotificationMessage) {

View File

@ -1,108 +1,155 @@
import { Extension } from '@codemirror/state' import { autocompletion } from '@codemirror/autocomplete'
import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view' import { Extension, EditorState, Prec } from '@codemirror/state'
import { import {
LanguageServerOptions, ViewPlugin,
LanguageServerClient, hoverTooltip,
lspPlugin, EditorView,
lspFormatCodeEvent, keymap,
} from '@kittycad/codemirror-lsp-client' KeyBinding,
import { deferExecution } from 'lib/utils' tooltips,
import { codeManager, editorManager, kclManager } from 'lib/singletons' } from '@codemirror/view'
import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams' import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams' import { offsetToPos } from 'editor/plugins/lsp/util'
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { LanguageServerOptions } from 'editor/plugins/lsp'
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' import { syntaxTree, indentService, foldService } from '@codemirror/language'
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
import {
LanguageServerPlugin,
documentUri,
languageId,
workspaceFolders,
} from 'editor/plugins/lsp/plugin'
const changesDelay = 600 export const kclIndentService = () => {
// Match the indentation of the previous line (if present).
// A view plugin that requests completions from the server after a delay return indentService.of((context, pos) => {
export class KclPlugin implements PluginValue { try {
private viewUpdate: ViewUpdate | null = null const previousLine = context.lineAt(pos, -1)
private client: LanguageServerClient const previousLineText = previousLine.text.replaceAll(
'\t',
constructor(client: LanguageServerClient) { ' '.repeat(context.state.tabSize)
this.client = client )
} const match = previousLineText.match(/^(\s)*/)
if (match === null || match.length <= 0) return null
private _deffererCodeUpdate = deferExecution(() => { return match[0].length
if (this.viewUpdate === null) { } catch (err) {
return console.error('Error in codemirror indentService', err)
} }
return null
kclManager.executeCode() })
}, changesDelay)
private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
editorManager.handleOnViewUpdate(this.viewUpdate)
}, 50)
update(viewUpdate: ViewUpdate) {
this.viewUpdate = viewUpdate
editorManager.setEditorView(viewUpdate.view)
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 (isUserSelect) {
this._deffererUserSelect(true)
return
}
if (!isRelevant) {
return
}
if (!viewUpdate.docChanged) {
return
}
const newCode = viewUpdate.state.doc.toString()
codeManager.code = newCode
codeManager.writeToFile()
this._deffererCodeUpdate(true)
}
async updateUnits(
params: UpdateUnitsParams
): Promise<UpdateUnitsResponse | null> {
return this.client.requestCustom('kcl/updateUnits', params)
}
async updateCanExecute(
params: UpdateCanExecuteParams
): Promise<UpdateCanExecuteResponse> {
return this.client.requestCustom('kcl/updateCanExecute', params)
}
} }
export function kclPlugin(options: LanguageServerOptions): Extension { export function kclPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null
const viewPlugin = ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
)
const kclKeymap: readonly KeyBinding[] = [
{
key: 'Alt-Shift-f',
run: (view: EditorView) => {
if (view.plugin === null) return false
// Get the current plugin from the map.
const p = view.plugin(viewPlugin)
if (p === null) return false
p.requestFormatting()
return true
},
},
]
// Create an extension for the key mappings.
const kclKeymapExt = Prec.highest(keymap.computeN([], () => [kclKeymap]))
const folding = foldService.of(
(state: EditorState, lineStart: number, lineEnd: number) => {
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)
}
)
return [ return [
lspPlugin(options), documentUri.of(options.documentUri),
ViewPlugin.define(() => new KclPlugin(options.client)), languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
viewPlugin,
kclKeymapExt,
kclIndentService(),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
null
),
tooltips({
position: 'absolute',
}),
linter((view) => {
let diagnostics: Diagnostic[] = []
forEachDiagnostic(
view.state,
(d: Diagnostic, from: number, to: number) => {
diagnostics.push(d)
}
)
return diagnostics
}),
folding,
autocompletion({
defaultKeymap: true,
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,
}
)
},
],
}),
] ]
} }

View File

@ -5,33 +5,18 @@ import {
defineLanguageFacet, defineLanguageFacet,
LanguageSupport, LanguageSupport,
} from '@codemirror/language' } from '@codemirror/language'
import { import { LanguageServerClient } from 'editor/plugins/lsp'
LanguageServerClient,
LanguageServerPlugin,
} from '@kittycad/codemirror-lsp-client'
import { kclPlugin } from '.' import { kclPlugin } from '.'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import KclParser from './parser' import { parser as jsParser } from '@lezer/javascript'
import { EditorState } from '@uiw/react-codemirror'
const data = defineLanguageFacet({ const data = defineLanguageFacet({})
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
})
export interface LanguageOptions { export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[] workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string documentUri: string
client: LanguageServerClient client: LanguageServerClient
processLspNotification?: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => void
} }
class KclLanguage extends Language { class KclLanguage extends Language {
@ -41,19 +26,36 @@ class KclLanguage extends Language {
workspaceFolders: options.workspaceFolders, workspaceFolders: options.workspaceFolders,
allowHTMLContent: true, allowHTMLContent: true,
client: options.client, client: options.client,
processLspNotification: options.processLspNotification,
}) })
const parser = new KclParser() super(
data,
super(data, parser, [plugin], 'kcl') // 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.
jsParser,
[
plugin,
EditorState.languageData.of(() => [
{
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
},
]),
],
'kcl'
)
} }
} }
export default class KclLanguageSupport extends LanguageSupport { export default function kclLanguage(options: LanguageOptions): LanguageSupport {
constructor(options: LanguageOptions) { const lang = new KclLanguage(options)
const lang = new KclLanguage(options)
super(lang) return new LanguageSupport(lang)
}
} }

View File

@ -1,6 +1,4 @@
// Extends the codemirror Parser for kcl. // Extends the codemirror Parser for kcl.
// This is really just a no-op parser since we use semantic tokens from the LSP
// server.
import { import {
Parser, Parser,
@ -9,27 +7,91 @@ import {
PartialParse, PartialParse,
Tree, Tree,
NodeType, NodeType,
NodeSet,
} from '@lezer/common' } from '@lezer/common'
import { LanguageServerClient } from 'editor/plugins/lsp'
import { posToOffset } from 'editor/plugins/lsp/util'
import { SemanticToken } from './semantic_tokens'
import { DocInput } from '@codemirror/language' import { DocInput } from '@codemirror/language'
import { tags, styleTags } from '@lezer/highlight'
export default class KclParser extends Parser { export default class KclParser extends Parser {
private client: LanguageServerClient
constructor(client: LanguageServerClient) {
super()
this.client = client
}
createParse( createParse(
input: Input, input: Input,
fragments: readonly TreeFragment[], fragments: readonly TreeFragment[],
ranges: readonly { from: number; to: number }[] ranges: readonly { from: number; to: number }[]
): PartialParse { ): PartialParse {
let parse: PartialParse = new Context(input) let parse: PartialParse = new Context(this, input, fragments, ranges)
return parse return parse
} }
getTokenTypes(): string[] {
return this.client.getServerCapabilities().semanticTokensProvider!.legend
.tokenTypes
}
getSemanticTokens(): SemanticToken[] {
return this.client.getSemanticTokens()
}
} }
class Context implements PartialParse { class Context implements PartialParse {
private parser: KclParser
private input: DocInput private input: DocInput
private fragments: readonly TreeFragment[]
private ranges: readonly { from: number; to: number }[]
private nodeTypes: { [key: string]: NodeType }
stoppedAt: number = 0 stoppedAt: number = 0
constructor(input: Input) { private semanticTokens: SemanticToken[] = []
private currentLine: number = 0
private currentColumn: number = 0
private nodeSet: NodeSet
constructor(
/// The parser configuration used.
parser: KclParser,
input: Input,
fragments: readonly TreeFragment[],
ranges: readonly { from: number; to: number }[]
) {
this.parser = parser
this.input = input as DocInput this.input = input as DocInput
this.fragments = fragments
this.ranges = ranges
// Iterate over the semantic token types and create a node type for each.
this.nodeTypes = {}
let nodeArray: NodeType[] = []
this.parser.getTokenTypes().forEach((tokenType, index) => {
const nodeType = NodeType.define({
id: index,
name: tokenType,
// props: [this.styleTags],
})
this.nodeTypes[tokenType] = nodeType
nodeArray.push(nodeType)
})
this.semanticTokens = this.parser.getSemanticTokens()
const styles = styleTags({
number: tags.number,
variable: tags.variableName,
operator: tags.operator,
keyword: tags.keyword,
string: tags.string,
comment: tags.comment,
function: tags.function(tags.variableName),
})
this.nodeSet = new NodeSet(nodeArray).extend(styles)
} }
get parsedPos(): number { get parsedPos(): number {
@ -37,8 +99,67 @@ class Context implements PartialParse {
} }
advance(): Tree | null { advance(): Tree | null {
if (this.semanticTokens.length === 0) {
return new Tree(NodeType.none, [], [], 0)
}
const tree = this.createTree(this.semanticTokens[0], 0)
this.stoppedAt = this.input.doc.length this.stoppedAt = this.input.doc.length
return new Tree(NodeType.none, [], [], this.input.doc.length) return tree
}
createTree(token: SemanticToken, index: number): Tree {
const changedLine = token.delta_line !== 0
this.currentLine += token.delta_line
if (changedLine) {
this.currentColumn = 0
}
this.currentColumn += token.delta_start
// Let's get our position relative to the start of the file.
let currentPosition = posToOffset(this.input.doc, {
line: this.currentLine,
character: this.currentColumn,
})
const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id]
if (currentPosition === undefined) {
// This is bad and weird.
return new Tree(nodeType, [], [], token.length)
}
if (index >= this.semanticTokens.length - 1) {
// We have no children.
return new Tree(nodeType, [], [], token.length)
}
const nextIndex = index + 1
const nextToken = this.semanticTokens[nextIndex]
const changedLineNext = nextToken.delta_line !== 0
const nextLine = this.currentLine + nextToken.delta_line
const nextColumn = changedLineNext
? nextToken.delta_start
: this.currentColumn + nextToken.delta_start
const nextPosition = posToOffset(this.input.doc, {
line: nextLine,
character: nextColumn,
})
if (nextPosition === undefined) {
// This is bad and weird.
return new Tree(nodeType, [], [], token.length)
}
// Let's get the
return new Tree(
nodeType,
[this.createTree(nextToken, nextIndex)],
// The positions (offsets relative to the start of this tree) of the children.
[nextPosition - currentPosition],
token.length
)
} }
stopAt(pos: number) { stopAt(pos: number) {

View File

@ -0,0 +1,51 @@
import type * as LSP from 'vscode-languageserver-protocol'
export class SemanticToken {
delta_line: number
delta_start: number
length: number
token_type: string
token_modifiers_bitset: string
constructor(
delta_line = 0,
delta_start = 0,
length = 0,
token_type = '',
token_modifiers_bitset = ''
) {
this.delta_line = delta_line
this.delta_start = delta_start
this.length = length
this.token_type = token_type
this.token_modifiers_bitset = token_modifiers_bitset
}
}
export async function deserializeTokens(
data: number[],
semanticTokensProvider?: LSP.SemanticTokensOptions
): Promise<SemanticToken[]> {
if (!semanticTokensProvider) {
return []
}
// Check if data length is divisible by 5
if (data.length % 5 !== 0) {
return Promise.reject(new Error('Length is not divisible by 5'))
}
const tokens = []
for (let i = 0; i < data.length; i += 5) {
tokens.push(
new SemanticToken(
data[i],
data[i + 1],
data[i + 2],
semanticTokensProvider.legend.tokenTypes[data[i + 3]],
semanticTokensProvider.legend.tokenModifiers[data[i + 4]]
)
)
}
return tokens
}

View File

@ -1,155 +1,115 @@
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
import { setDiagnostics } from '@codemirror/lint'
import { Facet } from '@codemirror/state'
import { EditorView, Tooltip } from '@codemirror/view'
import {
DiagnosticSeverity,
CompletionItemKind,
CompletionTriggerKind,
} from 'vscode-languageserver-protocol'
import { deferExecution } from 'lib/utils'
import type { import type {
Completion, Completion,
CompletionContext, CompletionContext,
CompletionResult, CompletionResult,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
import {
Facet,
StateEffect,
Extension,
Transaction,
Annotation,
} from '@codemirror/state'
import type {
ViewUpdate,
PluginValue,
PluginSpec,
ViewPlugin,
} from '@codemirror/view'
import { EditorView, Tooltip } from '@codemirror/view'
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
import type { ViewUpdate, PluginValue } from '@codemirror/view'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { import { LanguageServerClient } from 'editor/plugins/lsp'
DiagnosticSeverity, import { Marked } from '@ts-stack/markdown'
CompletionTriggerKind, import { posToOffset } from 'editor/plugins/lsp/util'
} from 'vscode-languageserver-protocol' import { Program, ProgramMemory } from 'lang/wasm'
import { URI } from 'vscode-uri' import { codeManager, editorManager, kclManager } from 'lib/singletons'
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
import { LanguageServerClient } from '../client' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
import { CompletionItemKindMap } from './autocomplete' import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
import { addToken, SemanticToken } from './semantic-tokens'
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
import lspAutocompleteExt from './autocomplete'
import lspHoverExt from './hover'
import lspFormatExt from './format'
import lspIndentExt from './indent'
import lspLintExt from './lint'
import lspSemanticTokensExt from './semantic-tokens'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '') const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const docPathFacet = Facet.define<string, string>({ export const documentUri = Facet.define<string, string>({ combine: useLast })
combine: useLast,
})
export const languageId = Facet.define<string, string>({ combine: useLast }) export const languageId = Facet.define<string, string>({ combine: useLast })
export const workspaceFolders = Facet.define< export const workspaceFolders = Facet.define<
LSP.WorkspaceFolder[], LSP.WorkspaceFolder[],
LSP.WorkspaceFolder[] LSP.WorkspaceFolder[]
>({ combine: useLast }) >({ combine: useLast })
export enum LspAnnotation { const CompletionItemKindMap = Object.fromEntries(
SemanticTokens = 'semantic-tokens', Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
FormatCode = 'format-code', ) as Record<CompletionItemKind, string>
Diagnostics = 'diagnostics',
}
const lspEvent = Annotation.define<LspAnnotation>() const changesDelay = 600
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens) let debounceTimer: ReturnType<typeof setTimeout> | null = null
export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode) const updateDelay = 100
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[]
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
processLspNotification?: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => void
changesDelay?: number
}
export class LanguageServerPlugin implements PluginValue { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
public documentUri: string
public languageId: string
public workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
private foldingRanges: LSP.FoldingRange[] | null = null private foldingRanges: LSP.FoldingRange[] | null = null
private viewUpdate: ViewUpdate | null = null
private previousSemanticTokens: SemanticToken[] = []
private allowHTMLContent: boolean = true
private changesDelay: number = 600
private processLspNotification?: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => void
private _defferer = deferExecution((code: string) => { private _defferer = deferExecution((code: string) => {
try { try {
// Update the state (not the editor) with the new code. // Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({ this.client.textDocumentDidChange({
textDocument: { textDocument: {
uri: this.getDocUri(), uri: this.documentUri,
version: this.documentVersion++, version: this.documentVersion++,
}, },
contentChanges: [{ text: code }], contentChanges: [{ text: code }],
}) })
this.requestSemanticTokens() if (this.viewUpdate) {
this.updateFoldingRanges() editorManager.handleOnViewUpdate(this.viewUpdate)
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
}, this.changesDelay) }, changesDelay)
constructor(options: LanguageServerOptions, private view: EditorView) { constructor(
this.client = options.client client: LanguageServerClient,
private view: EditorView,
private allowHTMLContent: boolean
) {
this.client = client
this.documentUri = this.view.state.facet(documentUri)
this.languageId = this.view.state.facet(languageId)
this.workspaceFolders = this.view.state.facet(workspaceFolders)
this.documentVersion = 0 this.documentVersion = 0
if (options.changesDelay) {
this.changesDelay = options.changesDelay
}
if (options.allowHTMLContent !== undefined) {
this.allowHTMLContent = options.allowHTMLContent
}
this.client.attachPlugin(this) this.client.attachPlugin(this)
this.processLspNotification = options.processLspNotification
this.initialize({ this.initialize({
documentText: this.getDocText(), documentText: this.view.state.doc.toString(),
}) })
} }
private getDocPath(view = this.view) {
return view.state.facet(docPathFacet)
}
private getDocText(view = this.view) {
return view.state.doc.toString()
}
private getDocUri(view = this.view) {
return URI.file(this.getDocPath(view)).toString()
}
private getLanguageId(view = this.view) {
return view.state.facet(languageId)
}
update(viewUpdate: ViewUpdate) { update(viewUpdate: ViewUpdate) {
// If the doc didn't change we can return early. this.viewUpdate = viewUpdate
if (!viewUpdate.docChanged) { if (!viewUpdate.docChanged) {
// debounce the view update.
// otherwise it is laggy for typing.
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
editorManager.handleOnViewUpdate(viewUpdate)
}, updateDelay)
return return
} }
const newCode = this.view.state.doc.toString()
codeManager.code = newCode
codeManager.writeToFile()
kclManager.executeCode()
this.sendChange({ this.sendChange({
documentText: viewUpdate.state.doc.toString(), documentText: newCode,
}) })
} }
@ -161,18 +121,14 @@ export class LanguageServerPlugin implements PluginValue {
if (this.client.initializePromise) { if (this.client.initializePromise) {
await this.client.initializePromise await this.client.initializePromise
} }
this.client.textDocumentDidOpen({ this.client.textDocumentDidOpen({
textDocument: { textDocument: {
uri: this.getDocUri(), uri: this.documentUri,
languageId: this.getLanguageId(), languageId: this.languageId,
text: documentText, text: documentText,
version: this.documentVersion, version: this.documentVersion,
}, },
}) })
this.requestSemanticTokens()
this.updateFoldingRanges()
} }
async sendChange({ documentText }: { documentText: string }) { async sendChange({ documentText }: { documentText: string }) {
@ -181,8 +137,8 @@ export class LanguageServerPlugin implements PluginValue {
this._defferer(documentText) this._defferer(documentText)
} }
requestDiagnostics() { requestDiagnostics(view: EditorView) {
this.sendChange({ documentText: this.getDocText() }) this.sendChange({ documentText: view.state.doc.toString() })
} }
async requestHoverTooltip( async requestHoverTooltip(
@ -195,9 +151,9 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ documentText: this.getDocText() }) this.sendChange({ documentText: view.state.doc.toString() })
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.documentUri },
position: { line, character }, position: { line, character },
}) })
if (!result) return null if (!result) return null
@ -213,8 +169,8 @@ export class LanguageServerPlugin implements PluginValue {
dom.classList.add('documentation') dom.classList.add('documentation')
dom.classList.add('hover-tooltip') dom.classList.add('hover-tooltip')
dom.style.zIndex = '99999999' dom.style.zIndex = '99999999'
if (this.allowHTMLContent) dom.innerHTML = formatMarkdownContents(contents) if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
else dom.textContent = formatMarkdownContents(contents) else dom.textContent = formatContents(contents)
return { pos, end, create: (view) => ({ dom }), above: true } return { pos, end, create: (view) => ({ dom }), above: true }
} }
@ -224,9 +180,8 @@ export class LanguageServerPlugin implements PluginValue {
!this.client.getServerCapabilities().foldingRangeProvider !this.client.getServerCapabilities().foldingRangeProvider
) )
return null return null
const result = await this.client.textDocumentFoldingRange({ const result = await this.client.textDocumentFoldingRange({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.documentUri },
}) })
return result || null return result || null
@ -267,6 +222,42 @@ export class LanguageServerPlugin implements PluginValue {
return null return null
} }
async updateUnits(units: UnitLength): Promise<UpdateUnitsResponse | null> {
if (this.client.name !== 'kcl') return null
if (!this.client.ready) return null
return await this.client.updateUnits({
textDocument: {
uri: this.documentUri,
},
text: this.view.state.doc.toString(),
units,
})
}
async updateCanExecute(
canExecute: boolean
): Promise<UpdateCanExecuteResponse | null> {
if (this.client.name !== 'kcl') return null
if (!this.client.ready) return null
let response = await this.client.updateCanExecute({
canExecute,
})
if (!canExecute && response.isExecuting) {
// We want to wait until the server is not busy before we reply to the
// caller.
while (response.isExecuting) {
await new Promise((resolve) => setTimeout(resolve, 100))
response = await this.client.updateCanExecute({
canExecute,
})
}
}
console.log('[lsp] kcl: updated canExecute', canExecute, response)
return response
}
async requestFormatting() { async requestFormatting() {
if ( if (
!this.client.ready || !this.client.ready ||
@ -276,14 +267,14 @@ export class LanguageServerPlugin implements PluginValue {
this.client.textDocumentDidChange({ this.client.textDocumentDidChange({
textDocument: { textDocument: {
uri: this.getDocUri(), uri: this.documentUri,
version: this.documentVersion++, version: this.documentVersion++,
}, },
contentChanges: [{ text: this.getDocText() }], contentChanges: [{ text: this.view.state.doc.toString() }],
}) })
const result = await this.client.textDocumentFormatting({ const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.documentUri },
options: { options: {
tabSize: 2, tabSize: 2,
insertSpaces: true, insertSpaces: true,
@ -291,16 +282,20 @@ export class LanguageServerPlugin implements PluginValue {
}, },
}) })
if (!result || !result.length) return null if (!result) return null
this.view.dispatch({ for (let i = 0; i < result.length; i++) {
changes: result.map(({ range, newText }) => ({ const { range, newText } = result[i]
from: posToOffset(this.view.state.doc, range.start)!, this.view.dispatch({
to: posToOffset(this.view.state.doc, range.end)!, changes: [
insert: newText, {
})), from: posToOffset(this.view.state.doc, range.start)!,
annotations: lspFormatCodeEvent, to: posToOffset(this.view.state.doc, range.end)!,
}) insert: newText,
},
],
})
}
} }
async requestCompletion( async requestCompletion(
@ -325,7 +320,7 @@ export class LanguageServerPlugin implements PluginValue {
}) })
const result = await this.client.textDocumentCompletion({ const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.documentUri },
position: { line, character }, position: { line, character },
context: { context: {
triggerKind, triggerKind,
@ -365,7 +360,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
if (documentation) { if (documentation) {
completion.info = () => { completion.info = () => {
const htmlString = formatMarkdownContents(documentation) const htmlString = formatContents(documentation)
const htmlNode = document.createElement('div') const htmlNode = document.createElement('div')
htmlNode.style.display = 'contents' htmlNode.style.display = 'contents'
htmlNode.innerHTML = htmlString htmlNode.innerHTML = htmlString
@ -384,107 +379,16 @@ export class LanguageServerPlugin implements PluginValue {
return completeFromList(options)(context) return completeFromList(options)(context)
} }
parseSemanticTokens(view: EditorView, data: number[]) {
// decode the lsp semantic token types
const tokens = []
for (let i = 0; i < data.length; i += 5) {
tokens.push({
deltaLine: data[i],
startChar: data[i + 1],
length: data[i + 2],
tokenType: data[i + 3],
modifiers: data[i + 4],
})
}
// convert the tokens into an array of {to, from, type} objects
const tokenTypes =
this.client.getServerCapabilities().semanticTokensProvider!.legend
.tokenTypes
const tokenModifiers =
this.client.getServerCapabilities().semanticTokensProvider!.legend
.tokenModifiers
const tokenRanges: any = []
let curLine = 0
let prevStart = 0
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
const tokenType = tokenTypes[token.tokenType]
// get a list of modifiers
const tokenModifier = []
for (let j = 0; j < tokenModifiers.length; j++) {
if (token.modifiers & (1 << j)) {
tokenModifier.push(tokenModifiers[j])
}
}
if (token.deltaLine !== 0) prevStart = 0
const tokenRange = {
from: posToOffset(view.state.doc, {
line: curLine + token.deltaLine,
character: prevStart + token.startChar,
})!,
to: posToOffset(view.state.doc, {
line: curLine + token.deltaLine,
character: prevStart + token.startChar + token.length,
})!,
type: tokenType,
modifiers: tokenModifier,
}
tokenRanges.push(tokenRange)
curLine += token.deltaLine
prevStart += token.startChar
}
// sort by from
tokenRanges.sort((a: any, b: any) => a.from - b.from)
return tokenRanges
}
async requestSemanticTokens() {
if (
!this.client.ready ||
!this.client.getServerCapabilities().semanticTokensProvider
) {
return null
}
const result = await this.client.textDocumentSemanticTokensFull({
textDocument: { uri: this.getDocUri() },
})
if (!result) return null
const { data } = result
this.previousSemanticTokens = this.parseSemanticTokens(this.view, data)
const effects: StateEffect<SemanticToken | Extension>[] =
this.previousSemanticTokens.map((tokenRange: any) =>
addToken.of(tokenRange)
)
this.view.dispatch({
effects,
annotations: [lspSemanticTokensEvent, Transaction.addToHistory.of(false)],
})
}
async processNotification(notification: LSP.NotificationMessage) { async processNotification(notification: LSP.NotificationMessage) {
try { try {
switch (notification.method) { switch (notification.method) {
case 'textDocument/publishDiagnostics': case 'textDocument/publishDiagnostics':
if (notification === undefined) break
if (notification.params === undefined) break
if (!notification.params) break
const params = notification.params as PublishDiagnosticsParams
if (!params) break
console.log( console.log(
'[lsp] [window/publishDiagnostics]', '[lsp] [window/publishDiagnostics]',
this.client.getName(), this.client.getName(),
params notification.params
) )
const params = notification.params as PublishDiagnosticsParams
// this is sometimes slower than our actual typing. // this is sometimes slower than our actual typing.
this.processDiagnostics(params) this.processDiagnostics(params)
break break
@ -502,17 +406,30 @@ export class LanguageServerPlugin implements PluginValue {
notification.params notification.params
) )
break break
case 'kcl/astUpdated':
// The server has updated the AST, we should update elsewhere.
let updatedAst = notification.params as Program
console.log('[lsp]: Updated AST', updatedAst)
// Update the folding ranges, since the AST has changed.
// This is a hack since codemirror does not support async foldService.
// When they do we can delete this.
this.updateFoldingRanges()
break
case 'kcl/memoryUpdated':
// The server has updated the memory, we should update elsewhere.
let updatedMemory = notification.params as ProgramMemory
console.log('[lsp]: Updated Memory', updatedMemory)
kclManager.programMemory = updatedMemory
break
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
// Send it to the plugin
this.processLspNotification?.(this, notification)
} }
processDiagnostics(params: PublishDiagnosticsParams) { processDiagnostics(params: PublishDiagnosticsParams) {
if (params.uri !== this.getDocUri()) return if (params.uri !== this.documentUri) return
const diagnostics = params.diagnostics const diagnostics = params.diagnostics
.map(({ range, message, severity }) => ({ .map(({ range, message, severity }) => ({
@ -542,26 +459,18 @@ export class LanguageServerPlugin implements PluginValue {
return 0 return 0
}) })
/* This creates infighting with the others. this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
* TODO: turn it back on when we have a better way to handle it.
* this.view.dispatch({
effects: [setDiagnosticsEffect.of(diagnostics)],
annotations: [lspDiagnosticsEvent, Transaction.addToHistory.of(false)],
})*/
} }
} }
export class LanguageServerPluginSpec function formatContents(
implements PluginSpec<LanguageServerPlugin> contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
{ ): string {
provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension { if (Array.isArray(contents)) {
return [ return contents.map((c) => formatContents(c) + '\n\n').join('')
lspAutocompleteExt(plugin), } else if (typeof contents === 'string') {
lspFormatExt(plugin), return Marked.parse(contents)
lspHoverExt(plugin), } else {
lspIndentExt(), return Marked.parse(contents.value)
lspLintExt(),
lspSemanticTokensExt(),
]
} }
} }

View File

@ -42,9 +42,8 @@ function registerServerCapability(
serverCapabilities: ServerCapabilities, serverCapabilities: ServerCapabilities,
registration: Registration registration: Registration
): ServerCapabilities | Error { ): ServerCapabilities | Error {
const serverCapabilitiesCopy = JSON.parse( const serverCapabilitiesCopy =
JSON.stringify(serverCapabilities) serverCapabilities as IFlexibleServerCapabilities
) as IFlexibleServerCapabilities
const { method, registerOptions } = registration const { method, registerOptions } = registration
const providerName = ServerCapabilitiesProviders[method] const providerName = ServerCapabilitiesProviders[method]
@ -52,10 +51,7 @@ function registerServerCapability(
if (!registerOptions) { if (!registerOptions) {
serverCapabilitiesCopy[providerName] = true serverCapabilitiesCopy[providerName] = true
} else { } else {
serverCapabilitiesCopy[providerName] = Object.assign( serverCapabilitiesCopy[providerName] = Object.assign({}, registerOptions)
{},
JSON.parse(JSON.stringify(registerOptions))
)
} }
} else { } else {
return new Error('Could not register server capability.') return new Error('Could not register server capability.')
@ -68,9 +64,8 @@ function unregisterServerCapability(
serverCapabilities: ServerCapabilities, serverCapabilities: ServerCapabilities,
unregistration: Unregistration unregistration: Unregistration
): ServerCapabilities { ): ServerCapabilities {
const serverCapabilitiesCopy = JSON.parse( const serverCapabilitiesCopy =
JSON.stringify(serverCapabilities) serverCapabilities as IFlexibleServerCapabilities
) as IFlexibleServerCapabilities
const { method } = unregistration const { method } = unregistration
const providerName = ServerCapabilitiesProviders[method] const providerName = ServerCapabilitiesProviders[method]

View File

@ -0,0 +1,21 @@
import { Message } from 'vscode-languageserver-protocol'
const env = import.meta.env.MODE
export default class Tracer {
static client(message: string): void {
// These are really noisy, so we have a special env var for them.
if (env === 'lsp_tracing') {
console.log('lsp client message', message)
}
}
static server(input: string | Message): void {
// These are really noisy, so we have a special env var for them.
if (env === 'lsp_tracing') {
const message: string =
typeof input === 'string' ? input : JSON.stringify(input)
console.log('lsp server message', message)
}
}
}

View File

@ -1,5 +1,3 @@
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength' import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
export enum LspWorker { export enum LspWorker {
@ -19,6 +17,11 @@ export interface CopilotWorkerOptions {
apiBaseUrl: string apiBaseUrl: string
} }
export enum LspWorkerEventType {
Init = 'init',
Call = 'call',
}
export interface LspWorkerEvent { export interface LspWorkerEvent {
eventType: LspWorkerEventType eventType: LspWorkerEventType
eventData: Uint8Array | KclWorkerOptions | CopilotWorkerOptions eventData: Uint8Array | KclWorkerOptions | CopilotWorkerOptions

View File

@ -0,0 +1,19 @@
import { Text } from '@codemirror/state'
export function posToOffset(
doc: Text,
pos: { line: number; character: number }
): number | undefined {
if (pos.line >= doc.lines) return
const offset = doc.line(pos.line + 1).from + pos.character
if (offset > doc.length) return
return offset
}
export function offsetToPos(doc: Text, offset: number) {
const line = doc.lineAt(offset)
return {
line: line.number - 1,
character: offset - line.from,
}
}

View File

@ -1,9 +1,4 @@
import { import { Codec, FromServer, IntoServer } from 'editor/plugins/lsp/codec'
Codec,
FromServer,
IntoServer,
LspWorkerEventType,
} from '@kittycad/codemirror-lsp-client'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import init, { import init, {
ServerConfig, ServerConfig,
@ -12,6 +7,7 @@ import init, {
} from 'wasm-lib/pkg/wasm_lib' } from 'wasm-lib/pkg/wasm_lib'
import * as jsrpc from 'json-rpc-2.0' import * as jsrpc from 'json-rpc-2.0'
import { import {
LspWorkerEventType,
LspWorkerEvent, LspWorkerEvent,
LspWorker, LspWorker,
KclWorkerOptions, KclWorkerOptions,

View File

@ -1,10 +1,9 @@
import { useLayoutEffect, useEffect, useRef } from 'react' import { useLayoutEffect, useEffect, useRef } from 'react'
import { useStore } from '../useStore'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' import { makeDefaultPlanes } from 'lang/wasm'
import { useModelingContext } from './useModelingContext'
import { useAppState } from 'AppState'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
@ -14,20 +13,24 @@ export function useSetupEngineManager(
theme: Themes.System, theme: Themes.System,
highlightEdges: true, highlightEdges: true,
enableSSAO: true, enableSSAO: true,
modelingSend: (() => {}) as any,
modelingContext: {} as any,
showScaleGrid: false,
} as { } as {
pool: string | null pool: string | null
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
enableSSAO: boolean enableSSAO: boolean
modelingSend: ReturnType<typeof useModelingContext>['send']
modelingContext: ReturnType<typeof useModelingContext>['context']
showScaleGrid: boolean
} }
) { ) {
const { setAppState } = useAppState() const {
setMediaStream,
setIsStreamReady,
setStreamDimensions,
streamDimensions,
} = useStore((s) => ({
setMediaStream: s.setMediaStream,
setIsStreamReady: s.setIsStreamReady,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions,
}))
const streamWidth = streamRef?.current?.offsetWidth const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight const streamHeight = streamRef?.current?.offsetHeight
@ -47,46 +50,26 @@ export function useSetupEngineManager(
streamWidth, streamWidth,
streamHeight streamHeight
) )
if ( if (!hasSetNonZeroDimensions.current && quadHeight && quadWidth) {
!hasSetNonZeroDimensions.current &&
quadHeight &&
quadWidth &&
settings.modelingSend
) {
engineCommandManager.start({ engineCommandManager.start({
setMediaStream: (mediaStream) => setMediaStream,
settings.modelingSend({ setIsStreamReady,
type: 'Set context',
data: { mediaStream },
}),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth, width: quadWidth,
height: quadHeight, height: quadHeight,
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.
kclManager.isFirstRender = true return kclManager.executeCode(true, true)
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
}, },
token, token,
settings, settings,
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager) return makeDefaultPlanes(kclManager.engineCommandManager)
}, },
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
}) })
settings.modelingSend({ setStreamDimensions({
type: 'Set context', streamWidth: quadWidth,
data: { streamHeight: quadHeight,
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
}) })
hasSetNonZeroDimensions.current = true hasSetNonZeroDimensions.current = true
} }
@ -95,7 +78,6 @@ export function useSetupEngineManager(
useLayoutEffect(startEngineInstance, [ useLayoutEffect(startEngineInstance, [
streamRef?.current?.offsetWidth, streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight, streamRef?.current?.offsetHeight,
settings.modelingSend,
]) ])
useEffect(() => { useEffect(() => {
@ -105,21 +87,16 @@ export function useSetupEngineManager(
streamRef?.current?.offsetHeight streamRef?.current?.offsetHeight
) )
if ( if (
settings.modelingContext.store.streamDimensions.streamWidth !== width || streamDimensions.streamWidth !== width ||
settings.modelingContext.store.streamDimensions.streamHeight !== height streamDimensions.streamHeight !== height
) { ) {
engineCommandManager.handleResize({ engineCommandManager.handleResize({
streamWidth: width, streamWidth: width,
streamHeight: height, streamHeight: height,
}) })
settings.modelingSend({ setStreamDimensions({
type: 'Set context', streamWidth: width,
data: { streamHeight: height,
streamDimensions: {
streamWidth: width,
streamHeight: height,
},
},
}) })
} }
}, 500) }, 500)

View File

@ -8,9 +8,9 @@ import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine' import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes' import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useStore } from 'useStore'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { useAppState } from 'AppState'
// This might not be necessary, AnyStateMachine from xstate is working // This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines = export type AllMachines =
@ -47,7 +47,9 @@ export default function useStateMachineCommands<
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState() const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
}))
useEffect(() => { useEffect(() => {
const disableAllButtons = const disableAllButtons =

View File

@ -260,8 +260,3 @@ 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

@ -1,4 +1,4 @@
import { executeAst, lintAst } from 'lang/langHelpers' import { executeAst, lintAst } from 'useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors' import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
@ -15,7 +15,6 @@ import {
ProgramMemory, ProgramMemory,
recast, recast,
SketchGroup, SketchGroup,
SourceRange,
ExtrudeGroup, ExtrudeGroup,
} from 'lang/wasm' } from 'lang/wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
@ -66,8 +65,6 @@ 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
} }
@ -197,11 +194,7 @@ 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()
@ -225,20 +218,12 @@ 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: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects object_ids: [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects padding: 0.1, // padding around the objects
}, },
}) })
@ -372,11 +357,6 @@ 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
@ -420,12 +400,7 @@ 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( await this.executeAst(astWithUpdatedSource)
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

Some files were not shown because too many files have changed in this diff Show More