Compare commits

...

29 Commits

Author SHA1 Message Date
8f138109dd Cut release v0.22.7 (#2826)
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-07-01 14:56:43 -07:00
8972f53256 Add setting to toggle scale grid visibility (#2838)
* Add a setting for showScaleGrid

* Fix up setting persistence, move under modeling

* Make the setting actually do something

* the lamest fmt I've seen in a while

* Fix clippy warnings

* Add snapshot tests for grid (first time using Playwright masks)

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

* Re-run CI after new screenshots added

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-01 12:31:42 -07:00
0c5b13ade5 Use PostCSS function from browser vendors to generate fallback color defs for when OKLCH isn't supported (#2770) 2024-07-01 12:29:08 -07:00
446f92a53a Rework zooming (#2798)
* Rework zooming

* Adjust sketch mode zoom

* Do not retry failures

* typo

* use sha as file upload id

* again

* again

* again

* again

* Fix camera moving too

* Use virtual fps instead of buffering for mouse

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-07-01 12:22:31 -07:00
2256e3bc09 Bump mime_guess from 2.0.4 to 2.0.5 in /src/wasm-lib (#2860)
Bumps [mime_guess](https://github.com/abonander/mime_guess) from 2.0.4 to 2.0.5.
- [Commits](https://github.com/abonander/mime_guess/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 11:31:43 -07:00
9e2876edc6 Bump serde_json from 1.0.118 to 1.0.119 in /src/wasm-lib (#2859)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.118 to 1.0.119.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.118...v1.0.119)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 11:31:29 -07:00
a138af1ec8 Revert "Workaround to fix tauri tests (#2772)" and remove tauri-action (#2861)
This reverts commit 6123ed6a82.
2024-07-01 11:26:04 -04:00
684c585a48 add more ghost text playwright tests (#2851)
add more ghost text tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-30 19:37:17 -07:00
500be20649 Move hide grid to rust (#2850)
* updates

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

* add back in to js side;

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

* order of operations

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

* fxi

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

* typos

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-30 19:21:24 -07:00
5fbbe2fa8c fixes up some playwright tests and adds a test for the ghost text plugin only in dev mode (#2849)
* things

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

* things

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

* updates

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

* fix up most tests

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

* fixups

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

* updates

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

* fixes

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

* fix lints

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

* updates

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

* typo

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-30 18:26:16 -07:00
5f5ecc5afe add a test for fold gutters (#2848)
* add a test for fold gutters

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

* typos

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-30 17:14:39 -07:00
3dafc31cad pull lsp client out into a fake module (#2846)
* initial commit

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

tsc passing

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

fixes

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

fixes

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

working

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

fixups

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

updates

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

fixes

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

fixes

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

fmt

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

* cleanups

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

* fixes

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

* udpates

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

* updates

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

* cleanup

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

* cleanup

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

* fixes

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-30 14:30:44 -07:00
9c230bc678 set plugin-fs to match and revert cli back to old version for release (#2847)
* revert tauri cli back and fix fs

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-30 13:20:27 -07:00
1fad6966b6 Bump @kittycad/lib from 0.0.67 to 0.0.69 (#2830)
Bumps [@kittycad/lib](https://github.com/KittyCAD/kittycad.ts) from 0.0.67 to 0.0.69.
- [Release notes](https://github.com/KittyCAD/kittycad.ts/releases)
- [Commits](https://github.com/KittyCAD/kittycad.ts/compare/v0.0.67...v0.0.69)

---
updated-dependencies:
- dependency-name: "@kittycad/lib"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-30 13:19:22 -07:00
c7efb4c006 codemirror lsp highlighter (#2806)
* tokenizer

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

updates

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

updates

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

fixes

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

updates

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

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

udates

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

fixes

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

updates

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

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

updates

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

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

updates

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

fixes

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

more cleaniup

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

updates

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

dont react to non relevant events

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

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

faster

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

cleanup code

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

defer

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

more events

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

fixes

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

updates

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

updates

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

updates

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

updates

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

cleanup

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

user events

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

updates

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

udpates

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

updates
;

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

upfates

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

updates

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

make highlighting code blocks easier

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

improvements

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

updates

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

updates

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

cleanup

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

updates

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

better builds

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

remove weird hacks

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

updates

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

better checks

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

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

make release builds in prod (#2839)

Update package.json

udpates

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

updates

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

fix some tests

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

updates

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

updates

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

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

updates

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

better timing

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

tweak delay

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

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

upfates

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

fixes

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

fixes

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

updates

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

updates

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

updates

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

ifxup

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

udpates

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

udpates

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

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

udpates

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

wait for the lsp for all screenshots so consistent

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

better playwright

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

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

fixes

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

Call core dump from the bug reporting button(s) (#2783)

*  Add coredump to refresh button - this one indicates that there should be something like a core dump that is triggered.
* Added lower right control bug report button - included custom toasts for bug reporting, supports fallback bug reporting when app cannot generate a core dump

better keymaps

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

emptu in comment

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

fix logs

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

fxes

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

add a test for tab to autocomplete

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

updates

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

better

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

printl

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

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

* updates

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

* updates

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

* upfates

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

* cleanup

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

* updates

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

* updates

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

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

* empty

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-29 18:10:07 -07:00
68fd921a64 playw tweaks (#2845) 2024-06-30 06:10:54 +10:00
a20e710e8f playw tweaks (#2843)
unused
2024-06-29 11:53:47 -07:00
9daf2d7794 make delete key work for solids (#2752)
* failing test

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

* failing test

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

* push up progress

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

* improve travers

* basic deleteFromSelection

* remove .only

* delete depended on extrude

* fix

* fix selection override

* add selection test

* Revert "add selection test"

This reverts commit 40a414b612.

* Revert "fix selection override"

This reverts commit 68e66e2980.

* more progress

* add toast message when we're not able to delet

* add e2e tests

* tweak test timeout

* more test tweaks

* fix back space cmd bar conflic

* clean up

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-06-30 03:36:04 +10:00
f86473d13b Call core dump from the bug reporting button(s) (#2783)
*  Add coredump to refresh button - this one indicates that there should be something like a core dump that is triggered.
* Added lower right control bug report button - included custom toasts for bug reporting, supports fallback bug reporting when app cannot generate a core dump
2024-06-28 18:06:40 -07:00
6fccc68c18 make release builds in prod (#2839)
Update package.json
2024-06-28 12:36:02 -07:00
ade66d0876 Bump ts-rs from 9.0.0 to 9.0.1 in /src/wasm-lib (#2837)
Bumps [ts-rs](https://github.com/Aleph-Alpha/ts-rs) from 9.0.0 to 9.0.1.
- [Release notes](https://github.com/Aleph-Alpha/ts-rs/releases)
- [Changelog](https://github.com/Aleph-Alpha/ts-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Aleph-Alpha/ts-rs/commits)

---
updated-dependencies:
- dependency-name: ts-rs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-27 23:34:32 -07:00
b5f3a067ee Selections bug (#2836)
* fix selection override

* add selection test

* fix playwright tests
2024-06-28 14:40:59 +10:00
bb9d24f821 Transformable patterns (#2824) 2024-06-27 22:20:51 -05:00
bd3cd97d74 move back to using dashmap and cleanup heaps of code (#2834)
* more

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

* fixups

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

* updates

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

* everything pre mutex locks

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

* remove clones

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

* another clone

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

* iupdates

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

* fixes

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

* updates

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

* progress

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

* more fixes

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

* cleanup

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

* updates

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

* updates

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

* updates

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

* test-utils

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

* fixes

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

* updates

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

* updates

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

* all features

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

* better naming

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

* upates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-27 15:43:49 -07:00
1b5839a7f8 More semantic tokens modifiers (#2823)
* more semantic tokens

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

* updates

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

* remove closed

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

* ficxes

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

* nuke more

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

* fix wasm

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-26 14:51:47 -07:00
a9e480f0ed Move walk handlers out of lint (#2822)
I want to make it more useful and generally applicable. I think in the
future we'll need a &mut variant, or an in-place tree replacer.
2024-06-26 16:32:30 -04:00
63fa04608c update onboarding KCL (#2820) 2024-06-26 13:09:53 -07:00
0d4d7fa751 Only show one error at once (#2801)
* Do not show more than one error toast at a time

* use sha as file upload id

* again

* again

* again

* again

* fmt

* Hopefully fix flakiness

* move to macos-14-large

---------

Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
Co-authored-by: Paul R. Tagliamonte <paul@kittycad.io>
2024-06-26 11:04:23 -07:00
68cdb68231 Implement stopping a walk from the Walk function (#2818)
When I originally wrote the walk stuff, I wanted to be able to stop a
traversal by returning false. That didn't get implemented in the first
rev, so this will actually build that out so returning false will stop
the walk.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2024-06-26 15:53:45 +00:00
162 changed files with 10802 additions and 3485 deletions

View File

@ -38,5 +38,7 @@ jobs:
- name: Benchmark kcl library - name: Benchmark kcl library
shell: bash shell: bash
run: |- run: |-
cd src/wasm-lib/kcl; cargo bench -- iai cd src/wasm-lib/kcl; cargo bench --all-features -- iai
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

View File

@ -238,12 +238,8 @@ 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' }}
with: run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
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' }}
@ -336,7 +332,6 @@ 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 }}
@ -348,8 +343,7 @@ 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' }}"
with: run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
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'
@ -367,7 +361,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' }}/app" E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-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)
@ -376,7 +370,7 @@ 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' }}\\app.exe" E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling 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

View File

@ -38,6 +38,8 @@ jobs:
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores
needs: check-rust-changes needs: check-rust-changes
steps: steps:
- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
@ -90,14 +92,17 @@ jobs:
- name: build web - name: build web
run: yarn build:local run: yarn build:local
- name: Run ubuntu/chrome snapshots - name: Run ubuntu/chrome snapshots
continue-on-error: true
run: | run: |
yarn playwright test --project="Google Chrome" --update-snapshots e2e/playwright/snapshot-tests.spec.ts yarn playwright test --project="Google Chrome" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
# remove test-results, messes with retry logic
rm -r test-results
env: env:
CI: true CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }} snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- name: Clean up test-results
if: always()
continue-on-error: true
run: rm -r test-results
- name: check for changes - name: check for changes
id: git-check id: git-check
run: | run: |
@ -124,7 +129,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true' if: steps.git-check.outputs.modified == 'true'
with: with:
name: playwright-report-ubuntu name: playwright-report-ubuntu-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
# if have previous run results, use them # if have previous run results, use them
@ -132,7 +137,7 @@ jobs:
if: always() if: always()
continue-on-error: true continue-on-error: true
with: with:
name: test-results-ubuntu name: test-results-ubuntu-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run ubuntu/chrome flow retry failures - name: Run ubuntu/chrome flow retry failures
id: retry id: retry
@ -158,23 +163,25 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: test-results-ubuntu name: test-results-ubuntu-${{ github.sha }}
path: test-results/ path: test-results/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: playwright-report-ubuntu name: playwright-report-ubuntu-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-14 runs-on: macos-14-large
needs: check-rust-changes needs: check-rust-changes
steps: steps:
- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
@ -232,7 +239,7 @@ jobs:
if: ${{ always() }} if: ${{ always() }}
continue-on-error: true continue-on-error: true
with: with:
name: test-results-macos name: test-results-macos-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run macos/safari flow retry failures - name: Run macos/safari flow retry failures
id: retry id: retry
@ -260,14 +267,14 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ always() }} if: ${{ always() }}
with: with:
name: test-results-macos name: test-results-macos-${{ github.sha }}
path: test-results/ path: test-results/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ always() }} if: ${{ always() }}
with: with:
name: playwright-report-macos name: playwright-report-macos-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true

2
.gitignore vendored
View File

@ -56,3 +56,5 @@ 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,5 +1,6 @@
# Ignore artifacts: # Ignore artifacts:
build build
dist
coverage coverage
# Ignore Rust projects: # Ignore Rust projects:
@ -9,5 +10,6 @@ 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

View File

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

View File

@ -55,6 +55,7 @@ layout: manual
* [`patternCircular3d`](kcl/patternCircular3d) * [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d) * [`patternLinear2d`](kcl/patternLinear2d)
* [`patternLinear3d`](kcl/patternLinear3d) * [`patternLinear3d`](kcl/patternLinear3d)
* [`patternTransform`](kcl/patternTransform)
* [`pi`](kcl/pi) * [`pi`](kcl/pi)
* [`pow`](kcl/pow) * [`pow`](kcl/pow)
* [`profileStart`](kcl/profileStart) * [`profileStart`](kcl/profileStart)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -91,8 +91,9 @@ 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')
@ -330,7 +331,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
@ -386,8 +387,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(
@ -443,7 +444,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()
@ -490,7 +491,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()
@ -589,7 +590,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()
@ -689,7 +690,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()
@ -739,7 +740,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()
@ -776,7 +777,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()
@ -795,3 +796,83 @@ 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: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 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

@ -45,8 +45,8 @@ async function clearCommandLogs(page: Page) {
await page.getByTestId('clear-commands').click() await page.getByTestId('clear-commands').click()
} }
async function expectCmdLog(page: Page, locatorStr: string) { async function expectCmdLog(page: Page, locatorStr: string, timeout = 5000) {
await expect(page.locator(locatorStr).last()).toBeVisible() await expect(page.locator(locatorStr).last()).toBeVisible({ timeout })
} }
async function waitForDefaultPlanesToBeVisible(page: Page) { async function waitForDefaultPlanesToBeVisible(page: Page) {
@ -207,6 +207,23 @@ 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()
@ -214,7 +231,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: () => waitForPageLoad(page), waitForAuthSkipAppStart: () => waitForAuthAndLsp(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]) => {
@ -228,7 +245,8 @@ export async function getUtils(page: Page) {
await fillInput('z', xyz[2]) await fillInput('z', xyz[2])
}, },
clearCommandLogs: () => clearCommandLogs(page), clearCommandLogs: () => clearCommandLogs(page),
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr), expectCmdLog: (locatorStr: string, timeout = 5000) =>
expectCmdLog(page, locatorStr, timeout),
openKclCodePanel: () => openKclCodePanel(page), openKclCodePanel: () => openKclCodePanel(page),
closeKclCodePanel: () => closeKclCodePanel(page), closeKclCodePanel: () => closeKclCodePanel(page),
openDebugPanel: () => openDebugPanel(page), openDebugPanel: () => openDebugPanel(page),
@ -300,11 +318,19 @@ export async function getUtils(page: Page) {
(screenshot.width * coords.y * pixMultiplier + (screenshot.width * coords.y * pixMultiplier +
coords.x * pixMultiplier) * coords.x * pixMultiplier) *
4 // rbga is 4 channels 4 // rbga is 4 channels
return Math.max( const maxDiff = Math.max(
Math.abs(screenshot.data[index] - expected[0]), Math.abs(screenshot.data[index] - expected[0]),
Math.abs(screenshot.data[index + 1] - expected[1]), Math.abs(screenshot.data[index + 1] - expected[1]),
Math.abs(screenshot.data[index + 2] - expected[2]) Math.abs(screenshot.data[index + 2] - expected[2])
) )
if (maxDiff > 4) {
console.log(
`Expected: ${expected} Actual: [${screenshot.data[index]}, ${
screenshot.data[index + 1]
}, ${screenshot.data[index + 2]}]`
)
}
return maxDiff
}, },
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) => doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
new Promise(async (resolve) => { new Promise(async (resolve) => {

View File

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

View File

@ -1,50 +1,43 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.22.6", "version": "0.22.7",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.3",
"@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",
"@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.67", "@kittycad/lib": "^0.0.69",
"@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.12", "@tauri-apps/api": "2.0.0-beta.12",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2", "@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.3", "@tauri-apps/plugin-fs": "^2.0.0-beta.5",
"@tauri-apps/plugin-http": "^2.0.0-beta.2", "@tauri-apps/plugin-http": "^2.0.0-beta.2",
"@tauri-apps/plugin-os": "^2.0.0-beta.3", "@tauri-apps/plugin-os": "^2.0.0-beta.3",
"@tauri-apps/plugin-process": "^2.0.0-beta.2", "@tauri-apps/plugin-process": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2", "@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@tauri-apps/plugin-updater": "^2.0.0-beta.3", "@tauri-apps/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", "@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",
"crypto-js": "^4.2.0", "codemirror": "^6.0.1",
"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.4.3", "html2canvas-pro": "^1.5.0",
"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",
@ -55,18 +48,14 @@
"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",
"swr": "^2.2.5",
"three": "^0.164.1", "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",
"wasm-pack": "^0.12.1", "vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2", "web-vitals": "^3.5.2",
"ws": "^8.17.0",
"xstate": "^4.38.2", "xstate": "^4.38.2",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
@ -85,11 +74,11 @@
"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", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"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 --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-clean": "yarn wasm-prep && yarn build:wasm", "build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings", "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
@ -122,12 +111,15 @@
"@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.44.1", "@playwright/test": "^1.44.1",
"@tauri-apps/cli": "^2.0.0-beta.13", "@tauri-apps/cli": "==2.0.0-beta.13",
"@types/crypto-js": "^4.2.2", "@testing-library/jest-dom": "^5.14.1",
"@types/debounce-promise": "^3.1.9", "@testing-library/react": "^15.0.2",
"@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",
@ -147,21 +139,27 @@
"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.0", "prettier": "^2.8.8",
"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.12.1",
"ws": "^8.17.0",
"yarn": "^1.22.22" "yarn": "^1.22.22"
} }
} }

View File

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

View File

@ -0,0 +1,35 @@
{
"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,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 PromiseMap from './map'
import Queue from './queue' import Queue from './queue'
import Tracer from '../tracer' import Tracer from './tracer'
import { Codec } from '../codec' import PromiseMap from './map'
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,9 +15,12 @@ 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() { constructor(trace?: boolean) {
super() super()
this.trace = trace || false
this.#start = this.start() this.#start = this.start()
} }
@ -64,7 +67,10 @@ 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) {
@ -85,7 +91,9 @@ 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
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) {

View File

@ -1,12 +1,16 @@
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 './codec/bytes' import Bytes from './bytes'
import StreamDemuxer from './codec/demuxer' import StreamDemuxer from './demuxer'
import Headers from './codec/headers' import Headers from './headers'
import Queue from './codec/queue' import Queue from './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()
@ -33,16 +37,24 @@ 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_: LspWorker | null = null private type_: String | 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 {
Tracer.client(Headers.remove(decoder.decode(item))) if (this.trace) {
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_,
@ -71,7 +83,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() return new StreamDemuxer(false)
} catch (e: any) { } catch (e: any) {
return e return e
} }

View File

@ -0,0 +1,13 @@
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,16 +1,8 @@
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client'
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens' import { FromServer, IntoServer } from './codec'
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin' import Client from './jsonrpc'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams' import { LanguageServerPlugin } from '../plugin/lsp'
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/
@ -31,12 +23,6 @@ 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
@ -49,21 +35,13 @@ 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 {
client: Client name: string
name: LspWorker fromServer: FromServer
} 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 {
@ -76,18 +54,18 @@ export class LanguageServerClient {
public initializePromise: Promise<void> public initializePromise: Promise<void>
private isUpdatingSemanticTokens: boolean = false
private semanticTokens: SemanticToken[] = []
private queuedUids: string[] = []
constructor(options: LanguageServerClientOptions) { constructor(options: LanguageServerClientOptions) {
this.plugins = []
this.client = options.client
this.name = options.name this.name = options.name
this.plugins = []
this.client = new Client(
options.fromServer,
options.intoServer,
options.initializedCallback
)
this.ready = false this.ready = false
this.queuedUids = []
this.initializePromise = this.initialize() this.initializePromise = this.initialize()
} }
@ -111,19 +89,10 @@ 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) {
@ -134,18 +103,9 @@ 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) {
@ -160,33 +120,13 @@ export class LanguageServerClient {
this.notify('workspace/didDeleteFiles', params) this.notify('workspace/didDeleteFiles', params)
} }
async updateSemanticTokens(uri: string) { async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) {
const serverCapabilities = this.getServerCapabilities() const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) { if (!serverCapabilities.semanticTokensProvider) {
return return
} }
// Make sure we can only run, if we aren't already running. return this.request('textDocument/semanticTokens/full', params)
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) {
@ -239,6 +179,10 @@ 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]
@ -246,44 +190,8 @@ export class LanguageServerClient {
return this.client.notify(method, params) return this.client.notify(method, params)
} }
async getCompletion(params: CopilotLspCompletionParams) { notifyCustom<P>(method: string, params: P): void {
const response = await this.request('copilot/getCompletions', params) return this.client.notify(method, 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

@ -6,7 +6,6 @@ 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: {
@ -67,8 +66,13 @@ 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(fromServer: FromServer, intoServer: IntoServer) { constructor(
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) => {
@ -82,6 +86,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
}) })
) )
this.#fromServer = fromServer this.#fromServer = fromServer
this.initializedCallback = initializedCallback
} }
async start(): Promise<void> { async start(): Promise<void> {
@ -124,7 +129,9 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
this.serverCapabilities, this.serverCapabilities,
capabilityRegistration capabilityRegistration
) )
if (err(caps)) return (this.serverCapabilities = {}) if (caps instanceof Error) {
return (this.serverCapabilities = {})
}
this.serverCapabilities = caps this.serverCapabilities = caps
} }
) )
@ -139,7 +146,9 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
this.serverCapabilities, this.serverCapabilities,
capabilityUnregistration capabilityUnregistration
) )
if (err(caps)) return (this.serverCapabilities = {}) if (caps instanceof Error) {
return (this.serverCapabilities = {})
}
this.serverCapabilities = caps this.serverCapabilities = caps
} }
) )
@ -151,7 +160,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
{ {
processId: null, processId: null,
clientInfo: { clientInfo: {
name: 'kcl-language-client', name: 'codemirror-lsp-client',
}, },
capabilities: client_capabilities, capabilities: client_capabilities,
rootUri: null, rootUri: null,
@ -163,6 +172,8 @@ 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

@ -0,0 +1,113 @@
import { autocompletion } from '@codemirror/autocomplete'
import { foldService, syntaxTree } from '@codemirror/language'
import { Extension, EditorState } from '@codemirror/state'
import { ViewPlugin } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import {
docPathFacet,
LanguageServerPlugin,
LanguageServerPluginSpec,
languageId,
workspaceFolders,
LanguageServerOptions,
} from './plugin/lsp'
import { offsetToPos } from './plugin/util'
export type { LanguageServerClientOptions } from './client'
export { LanguageServerClient } from './client'
export {
Codec,
FromServer,
IntoServer,
LspWorkerEventType,
} from './client/codec'
export type { LanguageServerOptions } from './plugin/lsp'
export type { TransactionInfo, RelevantUpdate } from './plugin/annotations'
export { updateInfo, TransactionAnnotation } from './plugin/annotations'
export {
LanguageServerPlugin,
LanguageServerPluginSpec,
docPathFacet,
languageId,
workspaceFolders,
} 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.
return plugin?.foldingRange(lineStart, lineEnd)
}),
]
if (options.client.getServerCapabilities().completionProvider) {
ext.push(
autocompletion({
defaultKeymap: false,
override: [
async (context) => {
if (plugin === null) {
return null
}
const { state, pos, explicit } = context
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
})
)
}
return ext
}

View File

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

View File

@ -0,0 +1,51 @@
import {
acceptCompletion,
clearSnippet,
closeCompletion,
hasNextSnippetField,
moveCompletionSelection,
nextSnippetField,
prevSnippetField,
startCompletion,
} from '@codemirror/autocomplete'
import { Prec } from '@codemirror/state'
import { EditorView, keymap, KeyBinding } from '@codemirror/view'
import { CompletionItemKind } from 'vscode-languageserver-protocol'
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,
},
]
export const lspAutocompleteKeymapExt = Prec.highest(
keymap.computeN([], () => [lspAutocompleteKeymap])
)

View File

@ -0,0 +1,27 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,12 @@
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,115 +1,148 @@
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 } 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 { LanguageServerClient } from 'editor/plugins/lsp' import {
import { Marked } from '@ts-stack/markdown' DiagnosticSeverity,
import { posToOffset } from 'editor/plugins/lsp/util' CompletionTriggerKind,
import { Program, ProgramMemory } from 'lang/wasm' } from 'vscode-languageserver-protocol'
import { codeManager, editorManager, kclManager } from 'lib/singletons' import { URI } from 'vscode-uri'
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { LanguageServerClient } from '../client'
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' import {
lspSemanticTokensEvent,
lspFormatCodeEvent,
relevantUpdate,
} from './annotations'
import { CompletionItemKindMap } from './autocomplete'
import { addToken, SemanticToken } from './semantic-tokens'
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
import { lspAutocompleteKeymapExt } from './autocomplete'
import 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 documentUri = Facet.define<string, string>({ combine: useLast }) export const docPathFacet = Facet.define<string, string>({
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 })
const CompletionItemKindMap = Object.fromEntries( export interface LanguageServerOptions {
Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) // We assume this is the main project directory, we are currently working in.
) as Record<CompletionItemKind, string> workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
processLspNotification?: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => void
const changesDelay = 600 changesDelay?: number
let debounceTimer: ReturnType<typeof setTimeout> | null = null }
const updateDelay = 100
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.documentUri, uri: this.getDocUri(),
version: this.documentVersion++, version: this.documentVersion++,
}, },
contentChanges: [{ text: code }], contentChanges: [{ text: code }],
}) })
if (this.viewUpdate) { this.requestSemanticTokens()
editorManager.handleOnViewUpdate(this.viewUpdate) this.updateFoldingRanges()
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
}, changesDelay) }, this.changesDelay)
constructor( constructor(options: LanguageServerOptions, private view: EditorView) {
client: LanguageServerClient, this.client = options.client
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.view.state.doc.toString(), documentText: this.getDocText(),
}) })
} }
update(viewUpdate: ViewUpdate) { private getDocPath(view = this.view) {
this.viewUpdate = viewUpdate return view.state.facet(docPathFacet)
if (!viewUpdate.docChanged) { }
// debounce the view update.
// otherwise it is laggy for typing.
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => { private getDocText(view = this.view) {
editorManager.handleOnViewUpdate(viewUpdate) return view.state.doc.toString()
}, updateDelay) }
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) {
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return return
} }
const newCode = this.view.state.doc.toString() // If the doc didn't change we can return early.
if (!viewUpdate.docChanged) {
codeManager.code = newCode return
codeManager.writeToFile() }
kclManager.executeCode()
this.sendChange({ this.sendChange({
documentText: newCode, documentText: viewUpdate.state.doc.toString(),
}) })
} }
@ -121,14 +154,18 @@ 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.documentUri, uri: this.getDocUri(),
languageId: this.languageId, languageId: this.getLanguageId(),
text: documentText, text: documentText,
version: this.documentVersion, version: this.documentVersion,
}, },
}) })
this.requestSemanticTokens()
this.updateFoldingRanges()
} }
async sendChange({ documentText }: { documentText: string }) { async sendChange({ documentText }: { documentText: string }) {
@ -137,8 +174,8 @@ export class LanguageServerPlugin implements PluginValue {
this._defferer(documentText) this._defferer(documentText)
} }
requestDiagnostics(view: EditorView) { requestDiagnostics() {
this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: this.getDocText() })
} }
async requestHoverTooltip( async requestHoverTooltip(
@ -151,9 +188,9 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: this.getDocText() })
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.getDocUri() },
position: { line, character }, position: { line, character },
}) })
if (!result) return null if (!result) return null
@ -169,8 +206,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 = formatContents(contents) if (this.allowHTMLContent) dom.innerHTML = formatMarkdownContents(contents)
else dom.textContent = formatContents(contents) else dom.textContent = formatMarkdownContents(contents)
return { pos, end, create: (view) => ({ dom }), above: true } return { pos, end, create: (view) => ({ dom }), above: true }
} }
@ -180,8 +217,9 @@ 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.documentUri }, textDocument: { uri: this.getDocUri() },
}) })
return result || null return result || null
@ -222,42 +260,6 @@ 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 ||
@ -267,14 +269,14 @@ export class LanguageServerPlugin implements PluginValue {
this.client.textDocumentDidChange({ this.client.textDocumentDidChange({
textDocument: { textDocument: {
uri: this.documentUri, uri: this.getDocUri(),
version: this.documentVersion++, version: this.documentVersion++,
}, },
contentChanges: [{ text: this.view.state.doc.toString() }], contentChanges: [{ text: this.getDocText() }],
}) })
const result = await this.client.textDocumentFormatting({ const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.getDocUri() },
options: { options: {
tabSize: 2, tabSize: 2,
insertSpaces: true, insertSpaces: true,
@ -287,13 +289,12 @@ export class LanguageServerPlugin implements PluginValue {
for (let i = 0; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
const { range, newText } = result[i] const { range, newText } = result[i]
this.view.dispatch({ this.view.dispatch({
changes: [ changes: {
{ from: posToOffset(this.view.state.doc, range.start)!,
from: posToOffset(this.view.state.doc, range.start)!, to: posToOffset(this.view.state.doc, range.end)!,
to: posToOffset(this.view.state.doc, range.end)!, insert: newText,
insert: newText, },
}, annotations: [lspFormatCodeEvent, Transaction.addToHistory.of(true)],
],
}) })
} }
} }
@ -320,7 +321,7 @@ export class LanguageServerPlugin implements PluginValue {
}) })
const result = await this.client.textDocumentCompletion({ const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.getDocUri() },
position: { line, character }, position: { line, character },
context: { context: {
triggerKind, triggerKind,
@ -360,7 +361,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
if (documentation) { if (documentation) {
completion.info = () => { completion.info = () => {
const htmlString = formatContents(documentation) const htmlString = formatMarkdownContents(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
@ -379,16 +380,107 @@ 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(),
notification.params 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
@ -406,30 +498,17 @@ 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.documentUri) return if (params.uri !== this.getDocUri()) return
const diagnostics = params.diagnostics const diagnostics = params.diagnostics
.map(({ range, message, severity }) => ({ .map(({ range, message, severity }) => ({
@ -459,18 +538,26 @@ export class LanguageServerPlugin implements PluginValue {
return 0 return 0
}) })
this.view.dispatch(setDiagnostics(this.view.state, diagnostics)) /* This creates infighting with the others.
* 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)],
})*/
} }
} }
function formatContents( export class LanguageServerPluginSpec
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] implements PluginSpec<LanguageServerPlugin>
): string { {
if (Array.isArray(contents)) { provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension {
return contents.map((c) => formatContents(c) + '\n\n').join('') return [
} else if (typeof contents === 'string') { lspAutocompleteKeymapExt,
return Marked.parse(contents) lspFormatExt(plugin),
} else { lspHoverExt(plugin),
return Marked.parse(contents.value) lspIndentExt(),
lspLintExt(),
lspSemanticTokensExt(),
]
} }
} }

View File

@ -0,0 +1,175 @@
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 './annotations'
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

@ -0,0 +1,55 @@
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

@ -0,0 +1,18 @@
{
"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

@ -0,0 +1,231 @@
# 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,
/* Retry on CI only */ /* Do not retry */
retries: process.env.CI ? 3 : 0, retries: process.env.CI ? 0 : 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,6 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
'@csstools/postcss-oklab-function': { preserve: true },
autoprefixer: {}, autoprefixer: {},
}, },
} }

164
src-tauri/Cargo.lock generated
View File

@ -1212,7 +1212,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.18" version = "0.1.20"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"convert_case 0.6.0", "convert_case 0.6.0",
@ -1260,6 +1260,15 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]] [[package]]
name = "dirs-next" name = "dirs-next"
version = "2.0.0" version = "2.0.0"
@ -1270,6 +1279,18 @@ dependencies = [
"dirs-sys-next", "dirs-sys-next",
] ]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "dirs-sys-next" name = "dirs-sys-next"
version = "0.1.2" version = "0.1.2"
@ -2576,7 +2597,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.67" version = "0.1.69"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -2946,9 +2967,9 @@ dependencies = [
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.13.1" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f428b4e9db3d17e2f809dfb1ff9ddfbbf16c71790d1656d10aee320877e1392f" checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
dependencies = [ dependencies = [
"cocoa", "cocoa",
"crossbeam-channel", "crossbeam-channel",
@ -3228,6 +3249,12 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordered-stream" name = "ordered-stream"
version = "0.2.0" version = "0.2.0"
@ -4396,9 +4423,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.17" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309" checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [ dependencies = [
"bigdecimal", "bigdecimal",
"bytes", "bytes",
@ -4414,9 +4441,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars_derive" name = "schemars_derive"
version = "0.8.17" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108" checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4546,9 +4573,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.116" version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
dependencies = [ dependencies = [
"indexmap 2.2.6", "indexmap 2.2.6",
"itoa 1.0.11", "itoa 1.0.11",
@ -5054,9 +5081,9 @@ dependencies = [
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.27.1" version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92bcf8885e147b56d6e26751263b45876284f32ca404703f6d3b8f80d16ff4dd" checksum = "ea538df05fbc2dcbbd740ba0cfe8607688535f4798d213cbbfa13ce494f3451f"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"cocoa", "cocoa",
@ -5085,8 +5112,8 @@ dependencies = [
"tao-macros", "tao-macros",
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"windows 0.56.0", "windows 0.57.0",
"windows-core 0.56.0", "windows-core 0.57.0",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
] ]
@ -5136,9 +5163,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.0.0-beta.17" version = "2.0.0-beta.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fedd5490eddf117253945f0baedafded43474c971cba546a818f527d5c26266" checksum = "5a258ecc5ac7ddade525f512c4962fd01cd0f5265e917b4572579c32c027bb31"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -5185,9 +5212,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.0.0-beta.13" version = "2.0.0-beta.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abcf98a9b4527567c3e5ca9723431d121e001c2145651b3fa044d22b5e025a7e" checksum = "82b964bb6d03d97e24e12f896aab463b02a3c2ff76a60f728cc37b5548eb470e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@ -5207,9 +5234,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.0.0-beta.13" version = "2.0.0-beta.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b383f341efb803852b0235a2f330ca90c4c113f422dd6d646b888685b372cace" checksum = "3529cfa977ed7c097f2a5e8da19ecffbe61982450a6c819e6165b6d0cfd3dd3a"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@ -5234,11 +5261,11 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.0.0-beta.13" version = "2.0.0-beta.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71be71718cfe48b149507157bfbad0e2ba0e98ea51658be26c7c677eb188fb0c" checksum = "36f97dd80334f29314aa5f40b5fad10cb9feffd08e5a5324fd728613841e5d33"
dependencies = [ dependencies = [
"heck 0.4.1", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.68",
@ -5248,9 +5275,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin" name = "tauri-plugin"
version = "2.0.0-beta.13" version = "2.0.0-beta.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6baaee0a083db1e04a1b7a3b0670d86a4d95dd2a54e7cbfb5547762b8ed098d9" checksum = "7c8385fd0a4f661f5652b0d9e2d7256187d553bb174f88564d10ebcfa6a3af53"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@ -5313,9 +5340,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.0.0-beta.7" version = "2.0.0-beta.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35377195c6923beda5f29482a16b492d431de964389fca9aaf81a0f7e908023f" checksum = "3aa91955751f329e0aa431b87c199b7378b6f91ec0765d2ad9d4c64e017c3cda"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@ -5466,9 +5493,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.0.0-beta.14" version = "2.0.0-beta.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "148b6e6aff8e63fe5d4ae1d50159d50cfc0b4309abdeca64833c887c6b5631ef" checksum = "d7dc96172a43536236ab55b7da7b8461bf75810985e668589e2395cb476937cb"
dependencies = [ dependencies = [
"dpi", "dpi",
"gtk", "gtk",
@ -5485,9 +5512,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.0.0-beta.14" version = "2.0.0-beta.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "398d065c6e0fbf3c4304583759b6e153bc1e0daeb033bede6834ebe4df371fc3" checksum = "5d4fd913b1f14a9b618c7f3ae35656d3aa759767fcb95b72006357c12b9d0b09"
dependencies = [ dependencies = [
"cocoa", "cocoa",
"gtk", "gtk",
@ -5509,16 +5536,15 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.0.0-beta.13" version = "2.0.0-beta.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4709765385f035338ecc330f3fba753b8ee283c659c235da9768949cdb25469" checksum = "4f24a9c20d676a3f025331cc1c3841256ba88c9f25fb7fae709d2b3089c50d90"
dependencies = [ dependencies = [
"brotli", "brotli",
"cargo_metadata", "cargo_metadata",
"ctor", "ctor",
"dunce", "dunce",
"glob", "glob",
"heck 0.5.0",
"html5ever", "html5ever",
"infer", "infer",
"json-patch", "json-patch",
@ -5994,14 +6020,14 @@ dependencies = [
[[package]] [[package]]
name = "tray-icon" name = "tray-icon"
version = "0.13.4" version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97ec55956c54569e74209ae9d29a7a79193b252d17a6ac28bcffd4c11a384ad" checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60"
dependencies = [ dependencies = [
"cocoa", "cocoa",
"core-graphics", "core-graphics",
"crossbeam-channel", "crossbeam-channel",
"dirs-next", "dirs",
"libappindicator", "libappindicator",
"muda", "muda",
"objc", "objc",
@ -6029,9 +6055,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "ts-rs" name = "ts-rs"
version = "9.0.0" version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357" checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde_json", "serde_json",
@ -6043,9 +6069,9 @@ dependencies = [
[[package]] [[package]]
name = "ts-rs-macros" name = "ts-rs-macros"
version = "9.0.0" version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577" checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -6522,8 +6548,8 @@ dependencies = [
"webview2-com-sys", "webview2-com-sys",
"windows 0.56.0", "windows 0.56.0",
"windows-core 0.56.0", "windows-core 0.56.0",
"windows-implement", "windows-implement 0.56.0",
"windows-interface", "windows-interface 0.56.0",
] ]
[[package]] [[package]]
@ -6611,6 +6637,16 @@ dependencies = [
"windows-targets 0.52.5", "windows-targets 0.52.5",
] ]
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"
@ -6626,8 +6662,20 @@ version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
dependencies = [ dependencies = [
"windows-implement", "windows-implement 0.56.0",
"windows-interface", "windows-interface 0.56.0",
"windows-result",
"windows-targets 0.52.5",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result", "windows-result",
"windows-targets 0.52.5", "windows-targets 0.52.5",
] ]
@ -6643,6 +6691,17 @@ dependencies = [
"syn 2.0.68", "syn 2.0.68",
] ]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.56.0" version = "0.56.0"
@ -6654,6 +6713,17 @@ dependencies = [
"syn 2.0.68", "syn 2.0.68",
] ]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.1" version = "0.1.1"
@ -6917,9 +6987,9 @@ dependencies = [
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.39.3" version = "0.40.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e180ac2740d6cb4d5cec0abf63eacbea90f1b7e5e3803043b13c1c84c4b7884" checksum = "1fa597526af53f310a8e6218630c5024fdde8271f229e70d7d2fc70b52b8fb1e"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block", "block",

View File

@ -24,7 +24,7 @@ tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" } tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-deep-link = { version = "2.0.0-beta.3" } 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.6" } tauri-plugin-fs = { version = "2.0.0-beta.9" }
tauri-plugin-http = { version = "2.0.0-beta.6" } tauri-plugin-http = { version = "2.0.0-beta.6" }
tauri-plugin-log = { version = "2.0.0-beta.4" } tauri-plugin-log = { version = "2.0.0-beta.4" }
tauri-plugin-os = { version = "2.0.0-beta.2" } tauri-plugin-os = { version = "2.0.0-beta.2" }

View File

@ -74,5 +74,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.22.6" "version": "0.22.7"
} }

View File

@ -25,6 +25,7 @@ import { LowerRightControls } from 'components/LowerRightControls'
import ModalContainer from 'react-modal-promise' import ModalContainer from 'react-modal-promise'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import Gizmo from 'components/Gizmo' import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump'
export function App() { export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS') useRefreshSettings(paths.FILE + 'SETTINGS')
@ -55,7 +56,11 @@ export function App() {
setHtmlRef(ref) setHtmlRef(ref)
}, [ref]) }, [ref])
const { settings } = useSettingsAuthContext() const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = new CoreDumpManager(engineCommandManager, ref, token)
const { const {
app: { onboardingStatus }, app: { onboardingStatus },
} = settings.context } = settings.context
@ -129,7 +134,7 @@ export function App() {
<ModelingSidebar paneOpacity={paneOpacity} /> <ModelingSidebar paneOpacity={paneOpacity} />
<Stream /> <Stream />
{/* <CamToggle /> */} {/* <CamToggle /> */}
<LowerRightControls> <LowerRightControls coreDumpManager={coreDumpManager}>
<Gizmo /> <Gizmo />
</LowerRightControls> </LowerRightControls>
</div> </div>

View File

@ -74,6 +74,9 @@ 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
@ -101,16 +104,12 @@ 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.debounceTimer = setTimeout(() => { this._isCamMovingCallback(false, false)
this._isCamMovingCallback(false, false)
}, 400) as any as number
} }
setCam = (camProps: ReactCameraProperties) => { setCam = (camProps: ReactCameraProperties) => {
@ -230,6 +229,7 @@ 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,6 +258,48 @@ 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',
@ -342,15 +384,7 @@ export class CameraControls {
if (interaction === 'none') return if (interaction === 'none') return
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
this.throttledEngCmd({ this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction,
window: { x: event.clientX, y: event.clientY },
},
cmd_id: uuidv4(),
})
return return
} }
@ -398,34 +432,19 @@ 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') {
const interactions = this.interactionGuards.zoom.scrollCallback( this.zoomDataFromLastFrame = event.deltaY
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.pendingZoom = this.pendingZoom ? this.pendingZoom : 1 this.handleStart()
this.pendingZoom *= 1 + event.deltaY * 0.01 this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001
this.handleEnd() this.handleEnd()
} }

View File

@ -568,6 +568,7 @@ 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,
@ -1967,9 +1968,9 @@ export async function getSketchOrientationDetails(
* @param entityId - The ID of the entity for which orientation details are being fetched. * @param entityId - The ID of the entity for which orientation details are being fetched.
* @returns A promise that resolves with the orientation details of the face. * @returns A promise that resolves with the orientation details of the face.
*/ */
async function getFaceDetails( export async function getFaceDetails(
entityId: string entityId: string
): Promise<Models['FaceIsPlanar_type']> { ): Promise<Models['GetSketchModePlane_type']> {
// TODO mode engine connection to allow batching returns and batch the following // TODO mode engine connection to allow batching returns and batch the following
await engineCommandManager.sendSceneCommand({ await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -1982,8 +1983,7 @@ async function getFaceDetails(
entity_id: entityId, entity_id: entityId,
}, },
}) })
// TODO change typing to get_sketch_mode_plane once lib is updated const faceInfo: Models['GetSketchModePlane_type'] = (
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({ await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

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

View File

@ -6,8 +6,18 @@ import { NetworkHealthIndicator } from 'components/NetworkHealthIndicator'
import { HelpMenu } from './HelpMenu' import { HelpMenu } from './HelpMenu'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { coreDump } from 'lang/wasm'
import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
export function LowerRightControls(props: React.PropsWithChildren) { export function LowerRightControls({
children,
coreDumpManager,
}: {
children?: React.ReactNode
coreDumpManager?: CoreDumpManager
}) {
const location = useLocation() const location = useLocation()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const linkOverrideClassName = const linkOverrideClassName =
@ -15,9 +25,42 @@ export function LowerRightControls(props: React.PropsWithChildren) {
const isPlayWright = window?.localStorage.getItem('playwright') === 'true' const isPlayWright = window?.localStorage.getItem('playwright') === 'true'
async function reportbug(event: { preventDefault: () => void }) {
event?.preventDefault()
if (!coreDumpManager) {
// open default reporting option
openWindow('https://github.com/KittyCAD/modeling-app/issues/new/choose')
} else {
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Preparing bug report...',
success: 'Bug report opened in new window',
error: 'Unable to export a core dump. Using default reporting.',
},
{
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,
},
}
)
.catch((err: Error) => {
if (err) {
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
)
}
})
}
}
return ( return (
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none"> <section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{props.children} {children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto"> <menu className="flex items-center justify-end gap-3 pointer-events-auto">
<a <a
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`} href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
@ -28,6 +71,7 @@ export function LowerRightControls(props: React.PropsWithChildren) {
v{isPlayWright ? '11.22.33' : APP_VERSION} v{isPlayWright ? '11.22.33' : APP_VERSION}
</a> </a>
<a <a
onClick={reportbug}
href="https://github.com/KittyCAD/modeling-app/issues/new/choose" href="https://github.com/KittyCAD/modeling-app/issues/new/choose"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -1,4 +1,3 @@
import { LanguageServerClient } from 'editor/plugins/lsp'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import React, { import React, {
createContext, createContext,
@ -7,10 +6,15 @@ import React, {
useContext, useContext,
useState, useState,
} from 'react' } from 'react'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' import {
import Client from '../editor/plugins/lsp/client' LanguageServerClient,
FromServer,
IntoServer,
LspWorkerEventType,
LanguageServerPlugin,
} from '@kittycad/codemirror-lsp-client'
import { TEST, VITE_KC_API_BASE_URL } from 'env' import { TEST, VITE_KC_API_BASE_URL } from 'env'
import kclLanguage from 'editor/plugins/lsp/kcl/language' import KclLanguageSupport 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 { useStore } from 'useStore'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -21,16 +25,15 @@ 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 { err } from 'lib/trap'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { isTauri } from 'lib/isTauri'
import { err, trap } from 'lib/trap' import { codeManager } from 'lib/singletons'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -75,13 +78,11 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
isCopilotLspServerReady, isCopilotLspServerReady,
setIsKclLspServerReady, setIsKclLspServerReady,
setIsCopilotLspServerReady, setIsCopilotLspServerReady,
isStreamReady,
} = useStore((s) => ({ } = useStore((s) => ({
isKclLspServerReady: s.isKclLspServerReady, isKclLspServerReady: s.isKclLspServerReady,
isCopilotLspServerReady: s.isCopilotLspServerReady, isCopilotLspServerReady: s.isCopilotLspServerReady,
setIsKclLspServerReady: s.setIsKclLspServerReady, setIsKclLspServerReady: s.setIsKclLspServerReady,
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady, setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
isStreamReady: s.isStreamReady,
})) }))
const [isLspReady, setIsLspReady] = useState(false) const [isLspReady, setIsLspReady] = useState(false)
const [isCopilotReady, setIsCopilotReady] = useState(false) const [isCopilotReady, setIsCopilotReady] = useState(false)
@ -96,8 +97,6 @@ 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.
@ -128,17 +127,34 @@ 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) const lspClient = new LanguageServerClient({
name: LspWorker.Kcl,
fromServer,
intoServer,
initializedCallback: () => {
setIsLspReady(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() && isKclLspServerReady && kclLspClient && codeManager.code) {
kclLspClient.textDocumentDidOpen({
textDocument: {
uri: `file:///${PROJECT_ENTRYPOINT}`,
languageId: 'kcl',
version: 1,
text: codeManager.code,
},
})
}
}, [kclLspClient, isKclLspServerReady])
// 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
@ -148,10 +164,30 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
let plugin = null let plugin = null
if (isKclLspServerReady && !TEST && kclLspClient) { if (isKclLspServerReady && !TEST && kclLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = kclLanguage({ const lsp = new KclLanguageSupport({
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
@ -159,27 +195,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
return plugin return plugin
}, [kclLspClient, isKclLspServerReady]) }, [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) {
return { lspClient: null } return { lspClient: null }
@ -205,13 +220,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: () => {
setIsCopilotReady(true)
},
}) })
return { lspClient } return { lspClient }
}, [token]) }, [token])

View File

@ -23,6 +23,7 @@ import {
editorManager, editorManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
angleBetweenInfo, angleBetweenInfo,
@ -70,7 +71,7 @@ import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror' import { EditorSelection, Transaction } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump' 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'
@ -78,6 +79,8 @@ import { getVarNameModal } from 'hooks/useToolbarGuards'
import useHotkeyWrapper from 'lib/hotkeyWrapper' 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 { modelingMachineEvent } from 'editor/manager'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -99,7 +102,7 @@ export const ModelingMachineProvider = ({
settings: { settings: {
context: { context: {
app: { theme, enableSSAO }, app: { theme, enableSSAO },
modeling: { defaultUnit, highlightEdges }, modeling: { defaultUnit, highlightEdges, showScaleGrid },
}, },
}, },
} = useSettingsAuthContext() } = useSettingsAuthContext()
@ -114,6 +117,7 @@ export const ModelingMachineProvider = ({
theme: theme.current, theme: theme.current,
highlightEdges: highlightEdges.current, highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current, enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current,
}) })
const { htmlRef } = useStore((s) => ({ const { htmlRef } = useStore((s) => ({
htmlRef: s.htmlRef, htmlRef: s.htmlRef,
@ -124,7 +128,6 @@ export const ModelingMachineProvider = ({
token token
) )
useHotkeyWrapper(['meta + shift + .'], () => { useHotkeyWrapper(['meta + shift + .'], () => {
console.warn('CoreDump: Initializing core dump')
toast.promise( toast.promise(
coreDump(coreDumpManager, true), coreDump(coreDumpManager, true),
{ {
@ -141,6 +144,7 @@ export const ModelingMachineProvider = ({
} }
) )
}) })
const { commandBarState } = useCommandsContext()
// Settings machine setup // Settings machine setup
// const retrievedSettings = useRef( // const retrievedSettings = useRef(
@ -279,11 +283,15 @@ 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) { if (!editorManager.editorView) return
editorManager.editorView.dispatch({ selection }) editorManager.editorView.dispatch({
} selection,
annotations: [
modelingMachineEvent,
Transaction.addToHistory.of(false),
],
})
}) })
} }
let selections: Selections = { let selections: Selections = {
@ -460,6 +468,11 @@ export const ModelingMachineProvider = ({
return canExtrudeSelection(selectionRanges) return canExtrudeSelection(selectionRanges)
}, },
'has valid selection for deletion': ({ selectionRanges }) => {
if (!commandBarState.matches('Closed')) return false
if (selectionRanges.codeBasedSelections.length <= 0) return false
return true
},
'Sketch is empty': ({ sketchDetails }) => { 'Sketch is empty': ({ sketchDetails }) => {
const node = getNodeFromPath<VariableDeclaration>( const node = getNodeFromPath<VariableDeclaration>(
kclManager.ast, kclManager.ast,
@ -923,6 +936,11 @@ export const ModelingMachineProvider = ({
} }
}, [modelingSend]) }, [modelingSend])
// Allow using the delete key to delete solids
useHotkeys(['backspace', 'delete', 'del'], () => {
modelingSend({ type: 'Delete selection' })
})
useStateMachineCommands({ useStateMachineCommands({
machineId: 'modeling', machineId: 'modeling',
state: modelingState, state: modelingState,

View File

@ -84,6 +84,10 @@ 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(() => {
@ -134,7 +138,6 @@ export const KclEditorPane = () => {
highlightSelectionMatches(), highlightSelectionMatches(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }), syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
rectangularSelection(), rectangularSelection(),
drawSelection(),
dropCursor(), dropCursor(),
interact({ interact({
rules: [ rules: [
@ -173,13 +176,7 @@ 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)
@ -192,9 +189,9 @@ export const KclEditorPane = () => {
value={initialCode.current} value={initialCode.current}
extensions={editorExtensions} extensions={editorExtensions}
theme={theme} theme={theme}
onCreateEditor={(_editorView) => onCreateEditor={(_editorView) => {
editorManager.setEditorView(_editorView) editorManager.setEditorView(_editorView)
} }}
indentWithTab={false} indentWithTab={false}
basicSetup={false} basicSetup={false}
/> />

View File

@ -1,6 +1,6 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { useCallback, useEffect, useState } from 'react' import { HTMLAttributes, useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { Tab } from '@headlessui/react' import { Tab } from '@headlessui/react'
@ -56,15 +56,19 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
bottomRight: 'hidden', bottomRight: 'hidden',
}} }}
> >
<div className={styles.grid + ' flex-1'}> <div id="app-sidebar" className={styles.grid + ' flex-1'}>
<ModelingSidebarSection panes={topPanes} /> <ModelingSidebarSection id="sidebar-top" panes={topPanes} />
<ModelingSidebarSection panes={bottomPanes} alignButtons="end" /> <ModelingSidebarSection
id="sidebar-bottom"
panes={bottomPanes}
alignButtons="end"
/>
</div> </div>
</Resizable> </Resizable>
) )
} }
interface ModelingSidebarSectionProps { interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> {
panes: SidebarPane[] panes: SidebarPane[]
alignButtons?: 'start' | 'end' alignButtons?: 'start' | 'end'
} }
@ -72,6 +76,8 @@ interface ModelingSidebarSectionProps {
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
@ -123,7 +129,7 @@ function ModelingSidebarSection({
}, [showDebugPanel.current, togglePane, openPanes]) }, [showDebugPanel.current, togglePane, openPanes])
return ( return (
<div className="group contents"> <div className={'group contents ' + className} {...props}>
<Tab.Group <Tab.Group
vertical vertical
selectedIndex={ selectedIndex={
@ -135,6 +141,7 @@ function ModelingSidebarSection({
}} }}
> >
<Tab.List <Tab.List
id={`${props.id}-ribbon`}
className={ className={
'pointer-events-auto ' + 'pointer-events-auto ' +
(alignButtons === 'start' (alignButtons === 'start'
@ -161,6 +168,7 @@ 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 ' +

View File

@ -1,7 +1,25 @@
import { coreDump } from 'lang/wasm'
import { CoreDumpManager } from 'lib/coredump'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { engineCommandManager } from 'lib/singletons'
import React from 'react'
import toast from 'react-hot-toast'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { useStore } from 'useStore'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const { htmlRef } = useStore((s) => ({
htmlRef: s.htmlRef,
}))
const coreDumpManager = new CoreDumpManager(
engineCommandManager,
htmlRef,
token
)
export function RefreshButton() {
async function refresh() { async function refresh() {
if (window && 'plausible' in window) { if (window && 'plausible' in window) {
const p = window.plausible as ( const p = window.plausible as (
@ -17,8 +35,26 @@ export function RefreshButton() {
}) })
} }
// Window may not be available in some environments toast
window?.location.reload() .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,
},
}
)
.then(() => {
// Window may not be available in some environments
window?.location.reload()
})
} }
return ( return (

View File

@ -134,6 +134,11 @@ 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

View File

@ -83,6 +83,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
if (!videoRef.current) return if (!videoRef.current) return
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,

View File

@ -1,16 +1,25 @@
import { StateField, StateEffect } from '@codemirror/state' import { StateField, StateEffect, Annotation } 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,13 +1,25 @@
import { hasNextSnippetField } from '@codemirror/autocomplete'
import { EditorView, ViewUpdate } from '@codemirror/view' import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, SelectionRange } from '@codemirror/state' import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager, sceneInfra } from 'lib/singletons' import { engineCommandManager } 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 } from './highlightextension' import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint' import {
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
@ -22,8 +34,6 @@ export default class EditorManager {
codeBasedSelections: [], codeBasedSelections: [],
} }
private _lastSelectionEvent: number | null = null
private _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 = () => {}
@ -57,10 +67,6 @@ 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
} }
@ -83,32 +89,39 @@ 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],
editorView?.state.doc.length || selection[1] this._editorView?.state.doc.length || selection[1]
) )
if (editorView) { if (this._editorView) {
editorView.dispatch({ this._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 {
if (!this.editorView) return this.setDiagnostics([])
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)
}) })
@ -122,9 +135,7 @@ export default class EditorManager {
uniqueDiagnostics.add(diagnostic) uniqueDiagnostics.add(diagnostic)
}) })
this.editorView.dispatch( this.setDiagnostics([...uniqueDiagnostics])
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
)
} }
undo() { undo() {
@ -174,48 +185,33 @@ 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 we are just fucking around in a snippet, return early and don't if (!this._editorView) {
// 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
// a ${} to the end of the function so that's fine.
if (hasNextSnippetField(viewUpdate.view.state)) {
return
}
if (this.editorView === null) {
this.setEditorView(viewUpdate.view) this.setEditorView(viewUpdate.view)
} }
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === this._lastSelection) { const ranges = viewUpdate?.state?.selection?.ranges || []
// onUpdate is noisy and is fired a lot by extensions if (ranges.length === 0) {
// since we're only interested in selections changes we can ignore most of these.
return return
} }
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'][] = [ const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool', 'Equip Line tool',
@ -264,7 +260,3 @@ export default class EditorManager {
) )
} }
} }
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -1,11 +1,16 @@
/// 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,
@ -17,17 +22,41 @@ 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 {
LanguageServerPlugin, TransactionAnnotation,
documentUri, offsetToPos,
posToOffset,
LanguageServerOptions,
LanguageServerClient,
docPathFacet,
languageId, languageId,
workspaceFolders, TransactionInfo,
} from 'editor/plugins/lsp/plugin' updateInfo,
RelevantUpdate,
lspPlugin,
} from '@kittycad/codemirror-lsp-client'
import { deferExecution } from 'lib/utils'
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'
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
@ -38,15 +67,10 @@ 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
@ -60,11 +84,24 @@ interface GhostText {
uuid: string uuid: string
} }
export const completionDecoration = StateField.define<CompletionState>({ 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
}
// We only care about transactions with effects.
if (!transaction.effects) {
return state
}
for (const effect of transaction.effects) { for (const effect of transaction.effects) {
if (effect.is(addSuggestion)) { if (effect.is(addSuggestion)) {
// When adding a suggestion, we set th ghostText // When adding a suggestion, we set th ghostText
@ -160,331 +197,482 @@ export const completionDecoration = StateField.define<CompletionState>({
), ),
}) })
const copilotEvent = Annotation.define<null>() export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
const infos = updateInfo(update)
/**************************************************************************** // Make sure we are not in a snippet
************************* COMMANDS ****************************************** if (infos.some((info: TransactionInfo) => info.inSnippet)) {
*****************************************************************************/ return {
overall: false,
const acceptSuggestionCommand = ( userSelect: false,
copilotClient: LanguageServerClient, time: null,
view: EditorView }
) => {
// We delete the ghost text and insert the suggestion.
// We also set the cursor to the end of the suggestion.
const ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
return false
} }
const ghostTextStart = ghostText.displayPos return {
const ghostTextEnd = ghostText.endGhostText overall: infos.some(
(info: TransactionInfo) =>
const actualTextStart = ghostText.startPos info.transaction.annotation(copilotPluginEvent.type) !== undefined ||
const actualTextEnd = ghostText.endPos info.annotations.includes(TransactionAnnotation.UserSelect) ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
const replacementEnd = ghostText.endReplacement info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
const suggestion = ghostText.text info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove)
view.dispatch({ ),
changes: { userSelect: infos.some((info: TransactionInfo) =>
from: ghostTextStart, info.annotations.includes(TransactionAnnotation.UserSelect)
to: ghostTextEnd, ),
insert: '', time: infos.length ? infos[0].time : null,
},
// 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
} }
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: clearSuggestion.of(null),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
copilotClient.reject()
return false
} }
const sameKeyCommand = ( // A view plugin that requests completions from the server after a delay
copilotClient: LanguageServerClient, export class CompletionRequester implements PluginValue {
view: EditorView, private client: LanguageServerClient
key: string private lastPos: number = 0
) => { private viewUpdate: ViewUpdate | null = null
// 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)
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) { private queuedUids: string[] = []
view.dispatch({
selection: { anchor: ghostTextStart + indent.length }, private _deffererCodeUpdate = deferExecution(() => {
effects: typeFirst.of(indent.length), if (this.viewUpdate === null) {
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], return
}) }
return true
} else if (key === 'Tab') { this.requestCompletions()
return acceptSuggestionCommand(copilotClient, view) }, changesDelay)
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
return rejectSuggestionCommand(copilotClient, view) private _deffererUserSelect = deferExecution(() => {
} else if (ghostText.displayText.length === 1) { if (this.viewUpdate === null) {
return acceptSuggestionCommand(copilotClient, view) return
} else { }
// Use this to delete the first letter of the suggestion
view.dispatch({ this.rejectSuggestionCommand()
selection: { anchor: ghostTextStart + 1 }, }, changesDelay)
effects: typeFirst.of(1),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], constructor(client: LanguageServerClient) {
this.client = client
}
update(viewUpdate: ViewUpdate) {
this.viewUpdate = viewUpdate
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
}
// If we have a user select event, we want to clear the ghost text.
if (isRelevant.userSelect) {
this._deffererUserSelect(true)
return
}
this.lastPos = this.viewUpdate.state.selection.main.head
this._deffererCodeUpdate(true)
}
ghostText(): GhostText | null {
if (!this.viewUpdate) {
return null
}
return (
this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null
)
}
containsGhostText(): boolean {
return this.ghostText() !== null
}
autocompleting(): boolean {
if (!this.viewUpdate) {
return false
}
return completionStatus(this.viewUpdate.state) === 'active'
}
notFocused(): boolean {
if (!this.viewUpdate) {
return true
}
return !this.viewUpdate.view.hasFocus
}
async requestCompletions(): Promise<void> {
if (
this.viewUpdate === null ||
this.containsGhostText() ||
this.autocompleting() ||
this.notFocused() ||
!this.viewUpdate.docChanged
) {
return
}
const pos = this.viewUpdate.state.selection.main.head
// Check if the position has changed
if (pos !== this.lastPos) {
return
}
// Get the current position and source
const state = this.viewUpdate.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.viewUpdate.view.state.doc.lineAt(pos)
if (line.to !== pos) {
const ending = this.viewUpdate.view.state.doc.sliceString(pos, line.to)
if (displayText.endsWith(ending)) {
displayText = displayText.slice(0, displayText.length - ending.length)
} else if (displayText.includes(ending)) {
// Remove the ending
this.viewUpdate.view.dispatch({
changes: {
from: pos,
to: line.to,
insert: '',
},
selection: { anchor: pos },
effects: typeFirst.of(ending.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
}
}
this.viewUpdate.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 {
if (!this.viewUpdate) {
return false
}
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.viewUpdate.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: acceptSuggestion.of(null),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
this.viewUpdate.view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
insert: suggestion,
},
selection: { anchor: actualTextEnd },
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
})
this.accept(ghostText.uuid)
return true return true
} }
rejectSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
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.viewUpdate.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: clearSuggestion.of(null),
annotations: [
rejectSuggestionCommand,
Transaction.addToHistory.of(false),
],
})
this.reject()
return false
}
sameKeyCommand(key: string) {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
const tabKey = 'Tab'
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
const ghostTextStart = ghostText.displayPos
const indent = this.viewUpdate.view.state.facet(indentUnit)
if (key === tabKey && ghostText.displayText.startsWith(indent)) {
this.viewUpdate.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.viewUpdate.view.dispatch({
selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
return true
}
}
async getCompletion(
params: CopilotLspCompletionParams
): 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) {
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.client.notifyCustom('copilot/notifyAccepted', params)
}
rejectCompletions(params: CopilotRejectCompletionParams) {
this.client.notifyCustom('copilot/notifyRejected', params)
}
} }
const completionPlugin = (copilotClient: LanguageServerClient) => export const copilotPlugin = (options: LanguageServerOptions): Extension => {
EditorView.domEventHandlers({ let plugin: CompletionRequester | null = null
const completionPlugin = ViewPlugin.define(
(view) => (plugin = new CompletionRequester(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'
) { ) {
return sameKeyCommand(copilotClient, view, event.key) if (view.plugin === null) return false
// 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 copilotAutocompleteKeymap: readonly KeyBinding[] = [
{
key: 'Tab',
run: (view: EditorView): boolean => {
if (view.plugin === null) return false
// Get the current plugin from the map.
const p = view.plugin(completionPlugin)
if (p === null) return false
return p.sameKeyCommand('Tab')
},
}, },
}) {
key: 'Backspace',
run: (view: EditorView): boolean => {
if (view.plugin === null) return false
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
const badUpdate = (update: ViewUpdate) => { return p.rejectSuggestionCommand()
for (const tr of update.transactions) { },
if (tr.annotation(copilotEvent) !== undefined) { },
return true {
} key: 'Delete',
} run: (view: EditorView): boolean => {
return false if (view.plugin === null) 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
}
return EditorView.updateListener.of((update: ViewUpdate) => { // Get the current plugin from the map.
if ( const p = view.plugin(completionPlugin)
update.docChanged && if (p === null) return false
!update.transactions.some((tr) =>
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 position and source return p.rejectSuggestionCommand()
const state = update.state },
const pos = state.selection.main.head },
const source = state.doc.toString() ]
const dUri = state.facet(documentUri) const copilotAutocompleteKeymapExt = Prec.highest(
const path = dUri.split('/').pop()! keymap.computeN([], () => [copilotAutocompleteKeymap])
const relativePath = dUri.replace('file://', '') )
// Set a new timeout to request completion
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 [
documentUri.of(options.documentUri), lspPlugin(options),
languageId.of('kcl'), completionPlugin,
workspaceFolders.of(options.workspaceFolders), copilotAutocompleteKeymapExt,
ViewPlugin.define( domHandlers,
(view) =>
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
),
completionDecoration, completionDecoration,
Prec.highest(completionPlugin(options.client)), EditorView.focusChangeEffect.of((_, focusing) => {
Prec.highest(viewCompletionPlugin(options.client)), if (plugin === null) return null
completionRequester(options.client),
plugin.rejectSuggestionCommand()
return null
}),
] ]
} }

View File

@ -1,155 +1,119 @@
import { autocompletion } from '@codemirror/autocomplete' import { Extension } from '@codemirror/state'
import { Extension, EditorState, Prec } from '@codemirror/state' import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view'
import { import {
ViewPlugin, LanguageServerOptions,
hoverTooltip, updateInfo,
EditorView, TransactionInfo,
keymap, RelevantUpdate,
KeyBinding, TransactionAnnotation,
tooltips, LanguageServerClient,
} from '@codemirror/view' lspPlugin,
import { CompletionTriggerKind } from 'vscode-languageserver-protocol' } from '@kittycad/codemirror-lsp-client'
import { offsetToPos } from 'editor/plugins/lsp/util' import { deferExecution } from 'lib/utils'
import { LanguageServerOptions } from 'editor/plugins/lsp' import { codeManager, editorManager, kclManager } from 'lib/singletons'
import { syntaxTree, indentService, foldService } from '@codemirror/language' import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams'
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint' import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams'
import { import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
LanguageServerPlugin, import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
documentUri,
languageId,
workspaceFolders,
} from 'editor/plugins/lsp/plugin'
export const kclIndentService = () => { const changesDelay = 600
// Match the indentation of the previous line (if present).
return indentService.of((context, pos) => { export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
try { const infos = updateInfo(update)
const previousLine = context.lineAt(pos, -1) // Make sure we are not in a snippet
const previousLineText = previousLine.text.replaceAll( if (infos.some((info: TransactionInfo) => info.inSnippet)) {
'\t', return {
' '.repeat(context.state.tabSize) overall: false,
) userSelect: false,
const match = previousLineText.match(/^(\s)*/) time: null,
if (match === null || match.length <= 0) return null
return match[0].length
} catch (err) {
console.error('Error in codemirror indentService', err)
} }
return null }
}) return {
overall: infos.some(
(info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect) ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove) ||
info.annotations.includes(TransactionAnnotation.FormatCode)
),
userSelect: infos.some((info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect)
),
time: infos.length ? infos[0].time : null,
}
}
// A view plugin that requests completions from the server after a delay
export class KclPlugin implements PluginValue {
private viewUpdate: ViewUpdate | null = null
private client: LanguageServerClient
constructor(client: LanguageServerClient) {
this.client = client
}
private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
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)
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
}
// If we have a user select event, we want to update what parts are
// highlighted.
if (isRelevant.userSelect) {
this._deffererUserSelect(true)
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 [
documentUri.of(options.documentUri), lspPlugin(options),
languageId.of('kcl'), ViewPlugin.define(() => new KclPlugin(options.client)),
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,18 +5,33 @@ import {
defineLanguageFacet, defineLanguageFacet,
LanguageSupport, LanguageSupport,
} from '@codemirror/language' } from '@codemirror/language'
import { LanguageServerClient } from 'editor/plugins/lsp' import {
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 { parser as jsParser } from '@lezer/javascript' import KclParser from './parser'
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 {
@ -26,36 +41,27 @@ 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( super(
data, data,
// For now let's use the javascript parser. // For now let's use the javascript parser.
// It works really well and has good syntax highlighting. // It works really well and has good syntax highlighting.
// We can use our lsp for the rest. // We can use our lsp for the rest.
jsParser, parser,
[ [plugin],
plugin,
EditorState.languageData.of(() => [
{
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
},
]),
],
'kcl' 'kcl'
) )
} }
} }
export default function kclLanguage(options: LanguageOptions): LanguageSupport { export default class KclLanguageSupport extends LanguageSupport {
const lang = new KclLanguage(options) constructor(options: LanguageOptions) {
const lang = new KclLanguage(options)
return new LanguageSupport(lang) super(lang)
}
} }

View File

@ -1,4 +1,6 @@
// 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,
@ -7,91 +9,27 @@ 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(this, input, fragments, ranges) let parse: PartialParse = new Context(input)
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
private semanticTokens: SemanticToken[] = [] constructor(input: Input) {
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 {
@ -99,67 +37,8 @@ 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 tree return new Tree(NodeType.none, [], [], this.input.doc.length)
}
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

@ -1,51 +0,0 @@
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,21 +0,0 @@
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,3 +1,5 @@
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 {
@ -17,11 +19,6 @@ 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

@ -1,19 +0,0 @@
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,4 +1,9 @@
import { Codec, FromServer, IntoServer } from 'editor/plugins/lsp/codec' import {
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,
@ -7,7 +12,6 @@ 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

@ -3,7 +3,7 @@ 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 } from 'lang/wasm' import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
@ -13,11 +13,13 @@ export function useSetupEngineManager(
theme: Themes.System, theme: Themes.System,
highlightEdges: true, highlightEdges: true,
enableSSAO: true, enableSSAO: true,
showScaleGrid: false,
} as { } as {
pool: string | null pool: string | null
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
enableSSAO: boolean enableSSAO: boolean
showScaleGrid: boolean
} }
) { ) {
const { const {
@ -66,6 +68,9 @@ export function useSetupEngineManager(
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager) return makeDefaultPlanes(kclManager.engineCommandManager)
}, },
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
}) })
setStreamDimensions({ setStreamDimensions({
streamWidth: quadWidth, streamWidth: quadWidth,

View File

@ -6,10 +6,13 @@ import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs' import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { KeyBinding } from '@uiw/react-codemirror' import { Annotation, KeyBinding, Transaction } from '@uiw/react-codemirror'
const PERSIST_CODE_TOKEN = 'persistCode' const PERSIST_CODE_TOKEN = 'persistCode'
const codeManagerUpdateAnnotation = Annotation.define<null>()
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(null)
export default class CodeManager { export default class CodeManager {
private _code: string = bracket private _code: string = bracket
#updateState: (arg: string) => void = () => {} #updateState: (arg: string) => void = () => {}
@ -90,6 +93,10 @@ export default class CodeManager {
to: editorManager.editorView.state.doc.length, to: editorManager.editorView.state.doc.length,
insert: code, insert: code,
}, },
annotations: [
codeManagerUpdateEvent,
Transaction.addToHistory.of(true),
],
}) })
} }
} }

View File

@ -1,6 +1,6 @@
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint' import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
import { posToOffset } from 'editor/plugins/lsp/util' import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol' import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state' import { Text } from '@codemirror/state'

View File

@ -15,6 +15,7 @@ import {
sketchOnExtrudedFace, sketchOnExtrudedFace,
deleteSegmentFromPipeExpression, deleteSegmentFromPipeExpression,
removeSingleConstraintInfo, removeSingleConstraintInfo,
deleteFromSelection,
} from './modifyAst' } from './modifyAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
@ -696,3 +697,196 @@ describe('Testing removeSingleConstraintInfo', () => {
}) })
}) })
}) })
describe('Testing deleteFromSelection', () => {
const cases = [
[
'basicCase',
{
codeBefore: `const myVar = 5
const sketch003 = startSketchOn('XZ')
|> startProfileAt([3.82, 13.6], %)
|> line([-2.94, 2.7], %)
|> line([7.7, 0.16], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
codeAfter: `const myVar = 5\n`,
lineOfInterest: 'line([-2.94, 2.7], %)',
type: 'default',
},
],
[
'delete extrude',
{
codeBefore: `const sketch001 = startSketchOn('XZ')
|> startProfileAt([3.29, 7.86], %)
|> line([2.48, 2.44], %)
|> line([2.66, 1.17], %)
|> line([3.75, 0.46], %)
|> line([4.99, -0.46], %, $seg01)
|> line([-3.86, -2.73], %)
|> line([-17.67, 0.85], %)
|> close(%)
const extrude001 = extrude(10, sketch001)`,
codeAfter: `const sketch001 = startSketchOn('XZ')
|> startProfileAt([3.29, 7.86], %)
|> line([2.48, 2.44], %)
|> line([2.66, 1.17], %)
|> line([3.75, 0.46], %)
|> line([4.99, -0.46], %, $seg01)
|> line([-3.86, -2.73], %)
|> line([-17.67, 0.85], %)
|> close(%)\n`,
lineOfInterest: 'line([2.66, 1.17], %)',
type: 'extrude-wall',
},
],
[
'delete extrude with sketch on it',
{
codeBefore: `const myVar = 5
const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.46, 5.12], %, $tag)
|> line([0.08, myVar], %)
|> line([13.03, 2.02], %, $seg01)
|> line([3.9, -7.6], %)
|> line([-11.18, -2.15], %)
|> line([5.41, -9.61], %)
|> line([-8.54, -2.51], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)
const sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.55, 2.89], %)
|> line([3.02, 1.9], %)
|> line([1.82, -1.49], %, $seg02)
|> angledLine([-86, segLen(seg02, %)], %)
|> line([-3.97, -0.53], %)
|> line([0.3, 0.84], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
codeAfter: `const myVar = 5
const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.46, 5.12], %, $tag)
|> line([0.08, myVar], %)
|> line([13.03, 2.02], %, $seg01)
|> line([3.9, -7.6], %)
|> line([-11.18, -2.15], %)
|> line([5.41, -9.61], %)
|> line([-8.54, -2.51], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn({
plane: {
origin: { x: 1, y: 2, z: 3 },
x_axis: { x: 4, y: 5, z: 6 },
y_axis: { x: 7, y: 8, z: 9 },
z_axis: { x: 10, y: 11, z: 12 }
}
})
|> startProfileAt([-12.55, 2.89], %)
|> line([3.02, 1.9], %)
|> line([1.82, -1.49], %, $seg02)
|> angledLine([-86, segLen(seg02, %)], %)
|> line([-3.97, -0.53], %)
|> line([0.3, 0.84], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`,
lineOfInterest: 'line([-11.18, -2.15], %)',
type: 'extrude-wall',
},
],
[
'delete extrude with sketch on it',
{
codeBefore: `const myVar = 5
const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.46, 5.12], %, $tag)
|> line([0.08, myVar], %)
|> line([13.03, 2.02], %, $seg01)
|> line([3.9, -7.6], %)
|> line([-11.18, -2.15], %)
|> line([5.41, -9.61], %)
|> line([-8.54, -2.51], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)
const sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.55, 2.89], %)
|> line([3.02, 1.9], %)
|> line([1.82, -1.49], %, $seg02)
|> angledLine([-86, segLen(seg02, %)], %)
|> line([-3.97, -0.53], %)
|> line([0.3, 0.84], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
codeAfter: `const myVar = 5
const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.46, 5.12], %, $tag)
|> line([0.08, myVar], %)
|> line([13.03, 2.02], %, $seg01)
|> line([3.9, -7.6], %)
|> line([-11.18, -2.15], %)
|> line([5.41, -9.61], %)
|> line([-8.54, -2.51], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn({
plane: {
origin: { x: 1, y: 2, z: 3 },
x_axis: { x: 4, y: 5, z: 6 },
y_axis: { x: 7, y: 8, z: 9 },
z_axis: { x: 10, y: 11, z: 12 }
}
})
|> startProfileAt([-12.55, 2.89], %)
|> line([3.02, 1.9], %)
|> line([1.82, -1.49], %, $seg02)
|> angledLine([-86, segLen(seg02, %)], %)
|> line([-3.97, -0.53], %)
|> line([0.3, 0.84], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`,
lineOfInterest: 'startProfileAt([4.46, 5.12], %, $tag)',
type: 'end-cap',
},
],
] as const
test.each(cases)(
'%s',
async (name, { codeBefore, codeAfter, lineOfInterest, type }) => {
// const lineOfInterest = 'line([-2.94, 2.7], %)'
const ast = parse(codeBefore)
if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
// deleteFromSelection
const range: [number, number] = [
codeBefore.indexOf(lineOfInterest),
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length,
]
const newAst = await deleteFromSelection(
ast,
{
range,
type,
},
programMemory,
async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
return {
origin: { x: 1, y: 2, z: 3 },
x_axis: { x: 4, y: 5, z: 6 },
y_axis: { x: 7, y: 8, z: 9 },
z_axis: { x: 10, y: 11, z: 12 },
}
}
)
if (err(newAst)) throw newAst
const newCode = recast(newAst)
expect(newCode).toBe(codeAfter)
}
)
})

View File

@ -17,6 +17,7 @@ import {
PathToNode, PathToNode,
ProgramMemory, ProgramMemory,
SourceRange, SourceRange,
SketchGroup,
} from './wasm' } from './wasm'
import { import {
isNodeSafeToReplacePath, isNodeSafeToReplacePath,
@ -25,6 +26,7 @@ import {
getNodeFromPath, getNodeFromPath,
getNodePathFromSourceRange, getNodePathFromSourceRange,
isNodeSafeToReplace, isNodeSafeToReplace,
traverse,
} from './queryAst' } from './queryAst'
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
import { import {
@ -38,6 +40,7 @@ import { isOverlap, roundOff } from 'lib/utils'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants' import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { ConstrainInfo } from './std/stdTypes' import { ConstrainInfo } from './std/stdTypes'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Program, node: Program,
@ -873,3 +876,175 @@ export function removeSingleConstraintInfo(
if (err(retval)) return false if (err(retval)) return false
return retval return retval
} }
export async function deleteFromSelection(
ast: Program,
selection: Selection,
programMemory: ProgramMemory,
getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () =>
({} as any)
): Promise<Program | Error> {
const astClone = JSON.parse(JSON.stringify(ast))
const range = selection.range
const path = getNodePathFromSourceRange(ast, range)
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
path,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (
(selection.type === 'extrude-wall' ||
selection.type === 'end-cap' ||
selection.type === 'start-cap') &&
varDec.node.init.type === 'PipeExpression'
) {
const varDecName = varDec.node.id.name
let pathToNode: PathToNode | null = null
let extrudeNameToDelete = ''
traverse(astClone, {
enter: (node, path) => {
if (node.type === 'VariableDeclaration') {
const dec = node.declarations[0]
if (
dec.init.type === 'CallExpression' &&
(dec.init.callee.name === 'extrude' ||
dec.init.callee.name === 'revolve') &&
dec.init.arguments?.[1].type === 'Identifier' &&
dec.init.arguments?.[1].name === varDecName
) {
pathToNode = path
extrudeNameToDelete = dec.id.name
}
}
},
})
if (!pathToNode) return new Error('Could not find extrude variable')
const expressionIndex = pathToNode[1][0] as number
astClone.body.splice(expressionIndex, 1)
if (extrudeNameToDelete) {
await new Promise(async (resolve) => {
let currentVariableName = ''
const pathsDependingOnExtrude: Array<{
path: PathToNode
sketchName: string
}> = []
traverse(astClone, {
leave: (node) => {
if (node.type === 'VariableDeclaration') {
currentVariableName = ''
}
},
enter: async (node, path) => {
if (node.type === 'VariableDeclaration') {
currentVariableName = node.declarations[0].id.name
}
if (
// match startSketchOn(${extrudeNameToDelete})
node.type === 'CallExpression' &&
node.callee.name === 'startSketchOn' &&
node.arguments[0].type === 'Identifier' &&
node.arguments[0].name === extrudeNameToDelete
) {
pathsDependingOnExtrude.push({
path,
sketchName: currentVariableName,
})
}
},
})
const roundLiteral = (x: number) => createLiteral(roundOff(x))
const modificationDetails: {
parent: PipeExpression['body']
faceDetails: Models['FaceIsPlanar_type']
lastKey: number
}[] = []
for (const { path, sketchName } of pathsDependingOnExtrude) {
const parent = getNodeFromPath<PipeExpression['body']>(
astClone,
path.slice(0, -1)
)
if (err(parent)) {
return
}
const sketchToPreserve = programMemory.root[sketchName] as SketchGroup
console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
if (
!(
faceDetails.origin &&
faceDetails.x_axis &&
faceDetails.y_axis &&
faceDetails.z_axis
)
) {
return
}
const lastKey = Number(path.slice(-1)[0][0])
modificationDetails.push({
parent: parent.node,
faceDetails,
lastKey,
})
}
for (const { parent, faceDetails, lastKey } of modificationDetails) {
if (
!(
faceDetails.origin &&
faceDetails.x_axis &&
faceDetails.y_axis &&
faceDetails.z_axis
)
) {
continue
}
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
createObjectExpression({
plane: createObjectExpression({
origin: createObjectExpression({
x: roundLiteral(faceDetails.origin.x),
y: roundLiteral(faceDetails.origin.y),
z: roundLiteral(faceDetails.origin.z),
}),
x_axis: createObjectExpression({
x: roundLiteral(faceDetails.x_axis.x),
y: roundLiteral(faceDetails.x_axis.y),
z: roundLiteral(faceDetails.x_axis.z),
}),
y_axis: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x),
y: roundLiteral(faceDetails.y_axis.y),
z: roundLiteral(faceDetails.y_axis.z),
}),
z_axis: createObjectExpression({
x: roundLiteral(faceDetails.z_axis.x),
y: roundLiteral(faceDetails.z_axis.y),
z: roundLiteral(faceDetails.z_axis.z),
}),
}),
}),
])
}
resolve(true)
})
}
// await prom
return astClone
} else if (varDec.node.init.type === 'PipeExpression') {
const pipeBody = varDec.node.init.body
if (
pipeBody[0].type === 'CallExpression' &&
pipeBody[0].callee.name === 'startSketchOn'
) {
// remove varDec
const varDecIndex = varDec.shallowPath[1][0] as number
astClone.body.splice(varDecIndex, 1)
return astClone
}
}
return new Error('Selection not recognised, could not delete')
}

View File

@ -19,7 +19,6 @@ import {
createPipeSubstitution, createPipeSubstitution,
} from './modifyAst' } from './modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { warn } from 'node:console'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise

View File

@ -282,8 +282,10 @@ function moreNodePathFromSourceRange(
} }
return path return path
} }
if (_node.type === 'PipeSubstitution' && isInRange) return path if (_node.type === 'PipeSubstitution' && isInRange) return path
console.error('not implemented: ' + node.type) console.error('not implemented: ' + node.type)
return path return path
} }

View File

@ -1143,6 +1143,7 @@ export class EngineCommandManager extends EventTarget {
this.getAst = cb this.getAst = cb
} }
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
start({ start({
setMediaStream, setMediaStream,
@ -1152,10 +1153,12 @@ export class EngineCommandManager extends EventTarget {
executeCode, executeCode,
token, token,
makeDefaultPlanes, makeDefaultPlanes,
modifyGrid,
settings = { settings = {
theme: Themes.Dark, theme: Themes.Dark,
highlightEdges: true, highlightEdges: true,
enableSSAO: true, enableSSAO: true,
showScaleGrid: false,
}, },
}: { }: {
setMediaStream: (stream: MediaStream) => void setMediaStream: (stream: MediaStream) => void
@ -1165,13 +1168,16 @@ export class EngineCommandManager extends EventTarget {
executeCode: () => void executeCode: () => void
token?: string token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes> makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void>
settings?: { settings?: {
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
enableSSAO: boolean enableSSAO: boolean
showScaleGrid: boolean
} }
}) { }) {
this.makeDefaultPlanes = makeDefaultPlanes this.makeDefaultPlanes = makeDefaultPlanes
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return return
} }
@ -1247,31 +1253,11 @@ export class EngineCommandManager extends EventTarget {
type: 'default_camera_get_settings', type: 'default_camera_get_settings',
}, },
}) })
// We want modify the grid first because we don't want it to flash.
this.initPlanes().then(async () => { // Ideally these would already be default hidden in engine (TODO do
// Hide the grid and grid scale text. // that) https://github.com/KittyCAD/engine/issues/2282
this.sendSceneCommand({ this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
type: 'modeling_cmd_req', await this.initPlanes()
cmd_id: uuidv4(),
cmd: {
type: 'object_visible' as any,
// Found in engine/constants.h
object_id: 'cfa78409-653d-4c26-96f1-7c45fb784840',
hidden: true,
},
})
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible' as any,
// Found in engine/constants.h
object_id: '10782f33-f588-4668-8bcd-040502d26590',
hidden: true,
},
})
this.resolveReady() this.resolveReady()
setIsStreamReady(true) setIsStreamReady(true)
await executeCode() await executeCode()
@ -1753,6 +1739,7 @@ export class EngineCommandManager extends EventTarget {
if ( if (
(cmd.type === 'camera_drag_move' || (cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move' || cmd.type === 'handle_mouse_drag_move' ||
cmd.type === 'default_camera_zoom' ||
cmd.type === ('default_camera_perspective_settings' as any)) && cmd.type === ('default_camera_perspective_settings' as any)) &&
this.engineConnection?.unreliableDataChannel && this.engineConnection?.unreliableDataChannel &&
!forceWebsocket !forceWebsocket
@ -2089,4 +2076,12 @@ export class EngineCommandManager extends EventTarget {
}, },
}) })
} }
/**
* Set the visibility of the scale grid in the engine scene.
* @param visible - whether to show or hide the scale grid
*/
setScaleGridVisibility(visible: boolean) {
this.modifyGrid(!visible)
}
} }

View File

@ -24,11 +24,7 @@ import {
isNotLiteralArrayOrStatic, isNotLiteralArrayOrStatic,
} from 'lang/std/sketchcombos' } from 'lang/std/sketchcombos'
import { toolTips, ToolTip } from '../../useStore' import { toolTips, ToolTip } from '../../useStore'
import { import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
createIdentifier,
createPipeExpression,
splitPathAtPipeExpression,
} from '../modifyAst'
import { import {
SketchLineHelper, SketchLineHelper,

View File

@ -9,6 +9,7 @@ import init, {
get_tangential_arc_to_info, get_tangential_arc_to_info,
program_memory_init, program_memory_init,
make_default_planes, make_default_planes,
modify_grid,
coredump, coredump,
toml_stringify, toml_stringify,
default_app_settings, default_app_settings,
@ -237,6 +238,20 @@ export const makeDefaultPlanes = async (
} }
} }
export const modifyGrid = async (
engineCommandManager: EngineCommandManager,
hidden: boolean
): Promise<void> => {
try {
await modify_grid(engineCommandManager, hidden)
return
} catch (e) {
// TODO: do something real with the error.
console.log('modify grid error', e)
return Promise.reject(e)
}
}
export function lexer(str: string): Token[] | Error { export function lexer(str: string): Token[] | Error {
return lexer_wasm(str) return lexer_wasm(str)
} }
@ -334,6 +349,7 @@ export async function coreDump(
openGithubIssue: boolean = false openGithubIssue: boolean = false
): Promise<CoreDumpInfo> { ): Promise<CoreDumpInfo> {
try { try {
console.warn('CoreDump: Initializing core dump')
const dump: CoreDumpInfo = await coredump(coreDumpManager) const dump: CoreDumpInfo = await coredump(coreDumpManager)
/* NOTE: this console output of the coredump should include the field /* NOTE: this console output of the coredump should include the field
`github_issue_url` which is not in the uploaded coredump file. `github_issue_url` which is not in the uploaded coredump file.

View File

@ -98,123 +98,119 @@ export type CommandConfig<
export type CommandArgumentConfig< export type CommandArgumentConfig<
OutputType, OutputType,
C = ContextFrom<AnyStateMachine> C = ContextFrom<AnyStateMachine>
> = > = {
description?: string
required:
| boolean
| ((
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: C
) => boolean)
skip?: boolean
} & (
| { | {
description?: string inputType: 'options'
required: options:
| boolean | CommandArgumentOption<OutputType>[]
| (( | ((
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency commandBarContext: {
argumentsToSubmit: Record<string, unknown>
}, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: C machineContext?: C
) => boolean) ) => CommandArgumentOption<OutputType>[])
skip?: boolean optionsFromContext?: (context: C) => CommandArgumentOption<OutputType>[]
} & ( defaultValue?:
| { | OutputType
inputType: 'options' | ((
options: commandBarContext: ContextFrom<typeof commandBarMachine>,
| CommandArgumentOption<OutputType>[] machineContext?: C
| (( ) => OutputType)
commandBarContext: { defaultValueFromContext?: (context: C) => OutputType
argumentsToSubmit: Record<string, unknown> }
}, // Should be the commandbarMachine's context, but it creates a circular dependency | {
machineContext?: C inputType: 'selection'
) => CommandArgumentOption<OutputType>[]) selectionTypes: Selection['type'][]
optionsFromContext?: ( multiple: boolean
context: C }
) => CommandArgumentOption<OutputType>[] | { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
defaultValue?: | {
| OutputType inputType: 'string'
| (( defaultValue?:
commandBarContext: ContextFrom<typeof commandBarMachine>, | OutputType
machineContext?: C | ((
) => OutputType) commandBarContext: ContextFrom<typeof commandBarMachine>,
defaultValueFromContext?: (context: C) => OutputType machineContext?: C
} ) => OutputType)
| { defaultValueFromContext?: (context: C) => OutputType
inputType: 'selection' }
selectionTypes: Selection['type'][] | {
multiple: boolean inputType: 'boolean'
} defaultValue?:
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values | OutputType
| { | ((
inputType: 'string' commandBarContext: ContextFrom<typeof commandBarMachine>,
defaultValue?: machineContext?: C
| OutputType ) => OutputType)
| (( defaultValueFromContext?: (context: C) => OutputType
commandBarContext: ContextFrom<typeof commandBarMachine>, }
machineContext?: C )
) => OutputType)
defaultValueFromContext?: (context: C) => OutputType
}
| {
inputType: 'boolean'
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => OutputType)
defaultValueFromContext?: (context: C) => OutputType
}
)
export type CommandArgument< export type CommandArgument<
OutputType, OutputType,
T extends AnyStateMachine = AnyStateMachine T extends AnyStateMachine = AnyStateMachine
> = > = {
description?: string
required:
| boolean
| ((
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: ContextFrom<T>
) => boolean)
skip?: boolean
machineActor: InterpreterFrom<T>
} & (
| { | {
description?: string inputType: Extract<CommandInputType, 'options'>
required: options:
| boolean | CommandArgumentOption<OutputType>[]
| (( | ((
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency commandBarContext: {
argumentsToSubmit: Record<string, unknown>
}, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: ContextFrom<T> machineContext?: ContextFrom<T>
) => boolean) ) => CommandArgumentOption<OutputType>[])
skip?: boolean defaultValue?:
machineActor: InterpreterFrom<T> | OutputType
} & ( | ((
| { commandBarContext: ContextFrom<typeof commandBarMachine>,
inputType: Extract<CommandInputType, 'options'> machineContext?: ContextFrom<T>
options: ) => OutputType)
| CommandArgumentOption<OutputType>[] }
| (( | {
commandBarContext: { inputType: 'selection'
argumentsToSubmit: Record<string, unknown> selectionTypes: Selection['type'][]
}, // Should be the commandbarMachine's context, but it creates a circular dependency multiple: boolean
machineContext?: ContextFrom<T> }
) => CommandArgumentOption<OutputType>[]) | { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
defaultValue?: | {
| OutputType inputType: 'string'
| (( defaultValue?:
commandBarContext: ContextFrom<typeof commandBarMachine>, | OutputType
machineContext?: ContextFrom<T> | ((
) => OutputType) commandBarContext: ContextFrom<typeof commandBarMachine>,
} machineContext?: ContextFrom<T>
| { ) => OutputType)
inputType: 'selection' }
selectionTypes: Selection['type'][] | {
multiple: boolean inputType: 'boolean'
} defaultValue?:
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value | OutputType
| { | ((
inputType: 'string' commandBarContext: ContextFrom<typeof commandBarMachine>,
defaultValue?: machineContext?: ContextFrom<T>
| OutputType ) => OutputType)
| (( }
commandBarContext: ContextFrom<typeof commandBarMachine>, )
machineContext?: ContextFrom<T>
) => OutputType)
}
| {
inputType: 'boolean'
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => OutputType)
}
)
export type CommandArgumentWithName< export type CommandArgumentWithName<
OutputType, OutputType,

View File

@ -13,6 +13,14 @@ import screenshot from 'lib/screenshot'
import React from 'react' import React from 'react'
import { VITE_KC_API_BASE_URL } from 'env' import { VITE_KC_API_BASE_URL } from 'env'
/* eslint-disable suggest-no-throw/suggest-no-throw --
* All the throws in CoreDumpManager are intentional and should be caught and handled properly
* by the calling Promises with a catch block. The throws are essential to properly handling
* when the app isn't ready enough or otherwise unable to produce a core dump. By throwing
* instead of simply erroring, the code halts execution at the first point which it cannot
* complete the core dump request.
**/
/** /**
* CoreDumpManager module * CoreDumpManager module
* - for getting all the values from the JS world to pass to the Rust world for a core dump. * - for getting all the values from the JS world to pass to the Rust world for a core dump.
@ -22,6 +30,7 @@ import { VITE_KC_API_BASE_URL } from 'env'
// CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts // CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts
// The async function coreDump() handles any errors thrown in its Promise catch method and rethrows // The async function coreDump() handles any errors thrown in its Promise catch method and rethrows
// them to so the toast handler in ModelingMachineProvider can show the user an error message toast // them to so the toast handler in ModelingMachineProvider can show the user an error message toast
// TODO: Throw more
export class CoreDumpManager { export class CoreDumpManager {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
htmlRef: React.RefObject<HTMLDivElement> | null htmlRef: React.RefObject<HTMLDivElement> | null

View File

@ -9,12 +9,12 @@ const wallMountL = 6 // the length of the bracket
const sigmaAllow = 35000 // psi const sigmaAllow = 35000 // psi
const width = 6 // inch const width = 6 // inch
const p = 300 // Force on shelf - lbs const p = 300 // Force on shelf - lbs
const L = 12 // inches const shelfLength = 12 // inches
const M = L * p / 2 // Moment experienced at fixed end of bracket const moment = shelfLength * p / 2 // Moment experienced at fixed end of bracket
const FOS = 2 // Factor of safety of 2 to be conservative const factorOfSafety = 2 // Factor of safety of 2 to be conservative
// Calculate the thickness off the bending stress and factor of safety // Calculate the thickness off the bending stress and factor of safety
const thickness = sqrt(6 * M * FOS / (width * sigmaAllow)) const thickness = sqrt(6 * moment * factorOfSafety / (width * sigmaAllow))
// 0.25 inch fillet radius // 0.25 inch fillet radius
const filletR = 0.25 const filletR = 0.25

View File

@ -325,6 +325,18 @@ export function createSettings() {
}, },
hideOnLevel: 'project', hideOnLevel: 'project',
}), }),
/**
* Whether to show a scale grid in the 3D modeling view
*/
showScaleGrid: new Setting<boolean>({
defaultValue: false,
description: 'Whether to show a scale grid in the 3D modeling view',
validate: (v) => typeof v === 'boolean',
commandConfig: {
inputType: 'boolean',
},
hideOnLevel: 'project',
}),
/** /**
* Whether to show the debug panel, which lets you see * Whether to show the debug panel, which lets you see
* various states of the app to aid in development * various states of the app to aid in development

View File

@ -48,6 +48,7 @@ function configurationToSettingsPayload(
), ),
highlightEdges: configuration?.settings?.modeling?.highlight_edges, highlightEdges: configuration?.settings?.modeling?.highlight_edges,
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel, showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
showScaleGrid: configuration?.settings?.modeling?.show_scale_grid,
}, },
textEditor: { textEditor: {
textWrapping: configuration?.settings?.text_editor?.text_wrapping, textWrapping: configuration?.settings?.text_editor?.text_wrapping,

View File

@ -105,6 +105,9 @@ export async function executor(
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes)) return new Promise((resolve) => resolve(defaultPlanes))
}, },
modifyGrid: (hidden: boolean) => {
return new Promise((resolve) => resolve())
},
}) })
await engineCommandManager.waitForReady await engineCommandManager.waitForReady
engineCommandManager.startNewSession() engineCommandManager.startNewSession()

View File

@ -29,7 +29,10 @@ export function cleanErrs<T>(
return [argsWOutErr.length !== value.length, argsWOutErr, argsWErr] return [argsWOutErr.length !== value.length, argsWOutErr, argsWErr]
} }
// Used to report errors to user at a certain point in execution /**
* Used to report errors to user at a certain point in execution
* @returns boolean
*/
export function trap<T>( export function trap<T>(
value: ExcludeErr<T> | Error, value: ExcludeErr<T> | Error,
opts?: { opts?: {
@ -43,6 +46,8 @@ export function trap<T>(
console.error(value) console.error(value)
opts?.suppress || opts?.suppress ||
toast.error((opts?.altErr ?? value ?? new Error('Unknown')).toString()) toast.error((opts?.altErr ?? value ?? new Error('Unknown')).toString(), {
id: 'error',
})
return true return true
} }

View File

@ -26,7 +26,11 @@ import {
applyConstraintEqualLength, applyConstraintEqualLength,
setEqualLengthInfo, setEqualLengthInfo,
} from 'components/Toolbar/EqualLength' } from 'components/Toolbar/EqualLength'
import { addStartProfileAt, extrudeSketch } from 'lang/modifyAst' import {
addStartProfileAt,
deleteFromSelection,
extrudeSketch,
} from 'lang/modifyAst'
import { getNodeFromPath } from '../lang/queryAst' import { getNodeFromPath } from '../lang/queryAst'
import { import {
applyConstraintEqualAngle, applyConstraintEqualAngle,
@ -44,12 +48,14 @@ import {
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { Vector3 } from 'three' import { Vector3 } from 'three'
import { quaternionFromUpNForward } from 'clientSideScene/helpers' import { quaternionFromUpNForward } from 'clientSideScene/helpers'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { Coords2d } from 'lang/std/sketch' import { Coords2d } from 'lang/std/sketch'
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp' import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
import { executeAst } from 'useStore'
import toast from 'react-hot-toast'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -157,6 +163,9 @@ export type ModelingMachineEvent =
type: 'Set selection' type: 'Set selection'
data: SetSelections data: SetSelections
} }
| {
type: 'Delete selection'
}
| { type: 'Sketch no face' } | { type: 'Sketch no face' }
| { type: 'Toggle gui mode' } | { type: 'Toggle gui mode' }
| { type: 'Cancel' } | { type: 'Cancel' }
@ -273,6 +282,13 @@ export const modelingMachine = createMachine(
cond: 'Has exportable geometry', cond: 'Has exportable geometry',
actions: 'Engine export', actions: 'Engine export',
}, },
'Delete selection': {
target: 'idle',
cond: 'has valid selection for deletion',
actions: ['AST delete selection'],
internal: true,
},
}, },
entry: 'reset client scene mouse handlers', entry: 'reset client scene mouse handlers',
@ -963,6 +979,42 @@ export const modelingMachine = createMachine(
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }
}, },
'AST delete selection': async ({ sketchDetails, selectionRanges }) => {
let ast = kclManager.ast
const getScaledFaceDetails = async (entityId: string) => {
const faceDetails = await getFaceDetails(entityId)
if (err(faceDetails)) return {}
return {
...faceDetails,
origin: {
x: faceDetails.origin.x / sceneInfra._baseUnitMultiplier,
y: faceDetails.origin.y / sceneInfra._baseUnitMultiplier,
z: faceDetails.origin.z / sceneInfra._baseUnitMultiplier,
},
}
}
const modifiedAst = await deleteFromSelection(
ast,
selectionRanges.codeBasedSelections[0],
kclManager.programMemory,
getScaledFaceDetails
)
if (err(modifiedAst)) return
const testExecute = await executeAst({
ast: modifiedAst,
useFakeExecutor: true,
engineCommandManager,
})
if (testExecute.errors.length) {
toast.error('Unable to delete part')
return
}
await kclManager.updateAst(modifiedAst, true)
},
'conditionally equip line tool': (_, { type }) => { 'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') { if (type === 'done.invoke.animate-to-face') {
sceneInfra.modelingSend('Equip Line tool') sceneInfra.modelingSend('Equip Line tool')

View File

@ -11,7 +11,7 @@ import {
export const settingsMachine = createMachine( export const settingsMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmsGxfCMPM08PQaDNU0cXRG1tLwedTaKxif7+UJTKIgGJzPCERZJFYpfDpLJgC6lK6VaqIEx6fBmCw2Do2IJ6MxdRDvTT4MRDdRGEzWbQ6ELTGGzOIIxLLVbrTbbNKYKBpLaitAAUWgcExMjk11uoBqVgM3jMYhsAIMrVs6ipPWChOeYhC9KMFhGHNh3IS5AASnB0AACZZw0SSS4KnF3PFafADTV1YZ2IxiH7dNpGfCaIzAgE+IzWMzBa1c+Y88gxZ3UYjEV3pvBysrem5VX0IFq0y3aTXWOp6JmU34IKMxuz0joGEYWsxp2IZu1kABUxexZdxtRG+EmQMZmne3dNBs0jKewLBsbCwI81n77vwtDAHHksDhBYHeDIEGYYEI2AAbowANZ3o8nzBnm3zMelpWqRAAFp62sJ4jEsZ4AT0UJGwjPFzH6cwNW0AwWXpbRImhbABXgUpvzwL0KgnCtgJMMCII8KCYLsA11EGOkXneDxmXMCk92hfCFlIQjFXLZUgLjddWhaFkgRCaxOhbEYzBnXwXkmOjAjjfduXfU9zzdOIeJ9fiEEA6ckwMClQ2BFpmJXMF9DjYI6hZfxmMw8IgA */ /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
id: 'Settings', id: 'Settings',
predictableActionArguments: true, predictableActionArguments: true,
context: {} as ReturnType<typeof createSettings>, context: {} as ReturnType<typeof createSettings>,
@ -32,6 +32,7 @@ export const settingsMachine = createMachine(
// No toast // No toast
actions: ['setSettingAtLevel'], actions: ['setSettingAtLevel'],
}, },
'set.app.themeColor': { 'set.app.themeColor': {
target: 'persisting settings', target: 'persisting settings',
@ -93,6 +94,15 @@ export const settingsMachine = createMachine(
'setClientTheme', 'setClientTheme',
], ],
}, },
'set.modeling.showScaleGrid': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineScaleGridVisibility',
],
},
}, },
}, },

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