Compare commits

..

55 Commits

Author SHA1 Message Date
89bb5e1a97 Trigger shutdown operations after each test 2024-07-05 15:10:18 -04:00
fe28527ef9 update docs (#2933)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-05 10:44:29 -07:00
0e8d0083c4 Cut release v0.23.1 (#2916)
* Cut release v0.23.1

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

* disable line tool if rectangle has started

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

* add test

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

* trigger ci

---------

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

* make it robust

* test hardening

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

* trigger CI

* harden test

---------

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

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

* Trigger CI

* Remove promises from getOsInfo for tauri apis

---------

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

"click plane to sketch on"

* Add margin and make the message text smaller

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

---------

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

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

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

* fmt

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

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

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

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

* updates

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

* change theme

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

* updates

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

* updates

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

* updates

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

* disable copilot temporarily

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

* fixes

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

* fixes

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

---------

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

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

* Dispatch only a single transaction in requestFormatting

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

* Remove old comment and some useless tests

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

* small codemirror changes from  marijnh

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

* fix some flaky tests

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

* fix

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Marijn Haverbeke <marijn@haverbeke.berlin>
2024-07-03 19:28:46 -07:00
c0f04d5f86 Cut release v0.23.0 (#2904) 2024-07-04 12:22:06 +10:00
3dbc701f26 remove bad scaling on delete (#2902)
remove weird scaling
2024-07-04 11:05:27 +10:00
16e7ae38e3 fix auto complete for circle (#2903)
crcle

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-03 18:05:24 -07:00
24c7260327 ctrl-c is copy, we should not bind to copy or paste or any common shit (#2895)
* ctrl-c is copy, we should not bind to copy or paste or any common shit

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

* fix tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-03 15:26:04 -07:00
72cfc4a471 fix version in vercel as main (#2899)
* fix version in vercel as nightly

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

* its actually the main branch

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

* format

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-03 15:24:17 -07:00
2d128ed32e Bump wasm-pack from 0.12.1 to 0.13.0 (#2887)
Bumps [wasm-pack](https://github.com/rustwasm/wasm-pack) from 0.12.1 to 0.13.0.
- [Release notes](https://github.com/rustwasm/wasm-pack/releases)
- [Changelog](https://github.com/rustwasm/wasm-pack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-pack/compare/v0.12.1...v0.13.0)

---
updated-dependencies:
- dependency-name: wasm-pack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 11:34:01 -07:00
cd6749ba02 Bump three from 0.164.1 to 0.166.1 (#2886)
Bumps [three](https://github.com/mrdoob/three.js) from 0.164.1 to 0.166.1.
- [Release notes](https://github.com/mrdoob/three.js/releases)
- [Commits](https://github.com/mrdoob/three.js/commits)

---
updated-dependencies:
- dependency-name: three
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 11:33:39 -07:00
7243405e1b Bump @playwright/test from 1.44.1 to 1.45.1 (#2885)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.44.1 to 1.45.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.44.1...v1.45.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 11:33:23 -07:00
c8da057ec2 Bump html2canvas-pro from 1.5.0 to 1.5.1 (#2888)
Bumps [html2canvas-pro](https://github.com/yorickshan/html2canvas-pro) from 1.5.0 to 1.5.1.
- [Release notes](https://github.com/yorickshan/html2canvas-pro/releases)
- [Changelog](https://github.com/yorickshan/html2canvas-pro/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yorickshan/html2canvas-pro/compare/v1.5.0...v1.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 11:14:32 -07:00
220fe5b2b8 bad code on exit-sketch should no delete code (#2890)
* bad code on exitsketch should no delete code

* clean up
2024-07-03 22:03:04 +10:00
4e6429de49 add fixme to failing test (#2891) 2024-07-03 20:54:58 +10:00
5391a65b18 Grouping tests (#2884)
test grouping
2024-07-03 14:34:45 +10:00
592628917a test grouping (#2883) 2024-07-03 12:41:24 +10:00
4c6e8633f7 zustand part 3 (#2878)
* zustand part 3

* clean up

* yarn lock
2024-07-02 16:22:46 -07:00
c5150468a2 more keybindings w copilot tests (#2875)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-02 10:08:02 -07:00
39126dbff1 fix copilot regression (#2876)
* fix copilot regression

* fix zoom part of test

* fix enable copilot
2024-07-02 08:50:40 -07:00
f86a69f12a initial migration from zustand (#2852)
* inital migration with a couple lingering concerns

* move is stream ready back

* put htmlRef back in useStore

* final tidy of useStore

* test tweaks

* tweak more

* more test tweaks

* fmt

* test tweaks

* attempts at fixing 'Basic default modeling and sketch hotkeys work'

* more tries

* 😭

* try again

* fmt
2024-07-02 17:16:27 +10:00
de354ee5d3 zustand updated for the lsp provider (#2874)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-01 21:43:15 -07:00
dfef7338ee disable copilot in sketch mode (#2865)
* disable copilot in sketch mode

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-01 21:05:31 -07:00
ee08948f54 add playwright tests for undo from click and point operations (#2866)
* add playwright tests for undo from click and point operations

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

* fix

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

* more determinitic

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-01 20:55:28 -07:00
832f6b65e2 Bump serde_json from 1.0.119 to 1.0.120 in /src/wasm-lib (#2872)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.119 to 1.0.120.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.119...v1.0.120)

---
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 20:34:46 -07:00
68efd77c5d fixes perms for releases (#2864)
* updates

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

* add to specific job

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-01 15:58:08 -07:00
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
160 changed files with 7454 additions and 5061 deletions

View File

@ -138,6 +138,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files - name: Copy updated .json files
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
@ -238,12 +239,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 +333,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 +344,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 +362,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,13 +371,15 @@ 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
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}' TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
publish-apps-release: publish-apps-release:
permissions:
contents: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps] needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]

2
.gitignore vendored
View File

@ -56,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

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

View File

@ -74,6 +74,10 @@ You can nest expressions in parenthesis as well:
let myMathExpression = 3 + (1 * 2 / (3 - 7)) let myMathExpression = 3 + (1 * 2 / (3 - 7))
``` ```
## Tag Declaration and Tag Identifiers
The syntax for tags is now `$myTag` to declare a tag and `myTag` to then use it later.
Please if you find any issues using any of the above expressions or syntax Please if you find any issues using any of the above expressions or syntax
please file an issue with the `ast` label on the [modeling-app please file an issue with the `ast` label on the [modeling-app
repo](https://github.com/KittyCAD/modeling-app/issues/new). repo](https://github.com/KittyCAD/modeling-app/issues/new).

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: 31 KiB

After

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -207,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,8 @@ 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),
waitForPageLoad: () => waitForPageLoad(page),
removeCurrentCode: () => removeCurrentCode(page), removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd), sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => { updateCamPosition: async (xyz: [number, number, number]) => {
@ -294,9 +312,9 @@ export async function getUtils(page: Page) {
fullPage: true, fullPage: true,
}) })
const screenshot = await PNG.sync.read(buffer) const screenshot = await PNG.sync.read(buffer)
// most likely related to pixel density but the screenshots for webkit are 2x the size const pixMultiplier: number = await page.evaluate(
// there might be a more robust way of doing this. 'window.devicePixelRatio'
const pixMultiplier = browserType === 'webkit' ? 2 : 1 )
const index = const index =
(screenshot.width * coords.y * pixMultiplier + (screenshot.width * coords.y * pixMultiplier +
coords.x * pixMultiplier) * coords.x * pixMultiplier) *
@ -359,11 +377,10 @@ export async function getUtils(page: Page) {
emulateNetworkConditions: async ( emulateNetworkConditions: async (
networkOptions: Protocol.Network.emulateNetworkConditionsParameters networkOptions: Protocol.Network.emulateNetworkConditionsParameters
) => { ) => {
// Skip on non-Chromium browsers, since we need to use the CDP. if (cdpSession === null) {
test.skip( // Use a fail safe if we can't simulate disconnect (on Safari)
cdpSession === null, return page.evaluate('window.tearDown()')
'Network emulation is only supported in Chromium' }
)
cdpSession?.send('Network.emulateNetworkConditions', networkOptions) cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
}, },

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.23.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0",
"@codemirror/language": "^6.10.2",
"@codemirror/lint": "^6.8.1",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@csstools/postcss-oklab-function": "^3.0.16",
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/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.14",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2", "@tauri-apps/plugin-dialog": "^2.0.0-beta.6",
"@tauri-apps/plugin-fs": "^2.0.0-beta.3", "@tauri-apps/plugin-fs": "^2.0.0-beta.6",
"@tauri-apps/plugin-http": "^2.0.0-beta.2", "@tauri-apps/plugin-http": "^2.0.0-beta.7",
"@tauri-apps/plugin-os": "^2.0.0-beta.3", "@tauri-apps/plugin-os": "^2.0.0-beta.6",
"@tauri-apps/plugin-process": "^2.0.0-beta.2", "@tauri-apps/plugin-process": "^2.0.0-beta.6",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2", "@tauri-apps/plugin-shell": "^2.0.0-beta.7",
"@tauri-apps/plugin-updater": "^2.0.0-beta.3", "@tauri-apps/plugin-updater": "^2.0.0-beta.6",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@testing-library/user-event": "^14.5.2",
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@tweenjs/tween.js": "^23.1.1",
"@types/node": "^18.19.31",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.2.25",
"@uiw/react-codemirror": "^4.21.25",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2", "@xstate/react": "^3.2.2",
"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.2",
"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,20 +48,15 @@
"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.166.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"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -85,8 +73,8 @@
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts", "test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e", "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 --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
@ -121,13 +109,16 @@
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.24.3", "@babel/preset-env": "^7.24.3",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.45.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 +138,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.13.0",
"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

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

View File

@ -0,0 +1,57 @@
import { foldService } from '@codemirror/language'
import { Extension, EditorState } from '@codemirror/state'
import { ViewPlugin } from '@codemirror/view'
import {
docPathFacet,
LanguageServerPlugin,
LanguageServerPluginSpec,
languageId,
workspaceFolders,
LanguageServerOptions,
} from './plugin/lsp'
export type { LanguageServerClientOptions } from './client'
export { LanguageServerClient } from './client'
export {
Codec,
FromServer,
IntoServer,
LspWorkerEventType,
} from './client/codec'
export type { LanguageServerOptions } from './plugin/lsp'
export {
LanguageServerPlugin,
LanguageServerPluginSpec,
docPathFacet,
languageId,
workspaceFolders,
lspSemanticTokensEvent,
lspDiagnosticsEvent,
lspFormatCodeEvent,
} from './plugin/lsp'
export { posToOffset, offsetToPos } from './plugin/util'
export function lspPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null
const viewPlugin = ViewPlugin.define(
(view) => (plugin = new LanguageServerPlugin(options, view)),
new LanguageServerPluginSpec()
)
let ext = [
docPathFacet.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
viewPlugin,
foldService.of((state: EditorState, lineStart: number, lineEnd: number) => {
if (plugin == null) return null
// Get the folding ranges from the language server.
// Since this is async we directly need to update the folding ranges after.
const range = plugin?.foldingRange(lineStart, lineEnd)
return range
}),
]
return ext
}

View File

@ -0,0 +1,112 @@
import {
acceptCompletion,
autocompletion,
clearSnippet,
closeCompletion,
hasNextSnippetField,
moveCompletionSelection,
nextSnippetField,
prevSnippetField,
startCompletion,
} from '@codemirror/autocomplete'
import { Prec, Extension } from '@codemirror/state'
import { EditorView, keymap, KeyBinding, ViewPlugin } from '@codemirror/view'
import {
CompletionItemKind,
CompletionTriggerKind,
} from 'vscode-languageserver-protocol'
import { LanguageServerPlugin } from './lsp'
import { offsetToPos } from './util'
import { syntaxTree } from '@codemirror/language'
export const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
) as Record<CompletionItemKind, string>
const lspAutocompleteKeymap: readonly KeyBinding[] = [
{ key: 'Ctrl-Space', run: startCompletion },
{
key: 'Escape',
run: (view: EditorView): boolean => {
if (clearSnippet(view)) return true
return closeCompletion(view)
},
},
{ key: 'ArrowDown', run: moveCompletionSelection(true) },
{ key: 'ArrowUp', run: moveCompletionSelection(false) },
{ key: 'PageDown', run: moveCompletionSelection(true, 'page') },
{ key: 'PageUp', run: moveCompletionSelection(false, 'page') },
{ key: 'Enter', run: acceptCompletion },
{
key: 'Tab',
run: (view: EditorView): boolean => {
if (hasNextSnippetField(view.state)) {
const result = nextSnippetField(view)
return result
}
return acceptCompletion(view)
},
shift: prevSnippetField,
},
]
const lspAutocompleteKeymapExt = Prec.highest(keymap.of(lspAutocompleteKeymap))
export default function lspAutocompleteExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
lspAutocompleteKeymapExt,
autocompletion({
defaultKeymap: false,
override: [
async (context) => {
const { state, pos, explicit, view } = context
let value = view?.plugin(plugin)
if (!value) return null
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
value.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await value.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
]
}

View File

@ -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,155 @@
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
import { setDiagnostics } from '@codemirror/lint'
import { Facet } from '@codemirror/state'
import { EditorView, Tooltip } from '@codemirror/view'
import {
DiagnosticSeverity,
CompletionItemKind,
CompletionTriggerKind,
} from 'vscode-languageserver-protocol'
import { deferExecution } from 'lib/utils'
import type { import type {
Completion, Completion,
CompletionContext, CompletionContext,
CompletionResult, CompletionResult,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
import {
Facet,
StateEffect,
Extension,
Transaction,
Annotation,
} from '@codemirror/state'
import type {
ViewUpdate,
PluginValue,
PluginSpec,
ViewPlugin,
} from '@codemirror/view'
import { EditorView, Tooltip } from '@codemirror/view'
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
import type { ViewUpdate, PluginValue } from '@codemirror/view'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { 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 { CompletionItemKindMap } from './autocomplete'
import { addToken, SemanticToken } from './semantic-tokens'
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
import lspAutocompleteExt from './autocomplete'
import lspHoverExt from './hover'
import lspFormatExt from './format'
import lspIndentExt from './indent'
import lspLintExt from './lint'
import lspSemanticTokensExt from './semantic-tokens'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '') const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const 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 enum LspAnnotation {
Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) SemanticTokens = 'semantic-tokens',
) as Record<CompletionItemKind, string> FormatCode = 'format-code',
Diagnostics = 'diagnostics',
}
const changesDelay = 600 const lspEvent = Annotation.define<LspAnnotation>()
let debounceTimer: ReturnType<typeof setTimeout> | null = null export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
const updateDelay = 100 export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
export interface LanguageServerOptions {
// We assume this is the main project directory, we are currently working in.
workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
processLspNotification?: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => void
changesDelay?: number
}
export class LanguageServerPlugin implements PluginValue { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
public documentUri: string
public languageId: string
public workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
private foldingRanges: LSP.FoldingRange[] | null = null private foldingRanges: LSP.FoldingRange[] | null = null
private viewUpdate: ViewUpdate | null = null
private previousSemanticTokens: SemanticToken[] = []
private allowHTMLContent: boolean = true
private changesDelay: number = 600
private processLspNotification?: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => void
private _defferer = deferExecution((code: string) => { private _defferer = deferExecution((code: string) => {
try { try {
// Update the state (not the editor) with the new code. // Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({ this.client.textDocumentDidChange({
textDocument: { textDocument: {
uri: this.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) {
// If the doc didn't change we can return early.
if (!viewUpdate.docChanged) {
return return
} }
const newCode = this.view.state.doc.toString()
codeManager.code = newCode
codeManager.writeToFile()
kclManager.executeCode()
this.sendChange({ this.sendChange({
documentText: newCode, documentText: viewUpdate.state.doc.toString(),
}) })
} }
@ -121,14 +161,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 +181,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 +195,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 +213,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 +224,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 +267,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 +276,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,
@ -282,20 +291,16 @@ export class LanguageServerPlugin implements PluginValue {
}, },
}) })
if (!result) return null if (!result || !result.length) return null
for (let i = 0; i < result.length; i++) { this.view.dispatch({
const { range, newText } = result[i] changes: result.map(({ range, newText }) => ({
this.view.dispatch({ from: posToOffset(this.view.state.doc, range.start)!,
changes: [ to: posToOffset(this.view.state.doc, range.end)!,
{ insert: newText,
from: posToOffset(this.view.state.doc, range.start)!, })),
to: posToOffset(this.view.state.doc, range.end)!, annotations: lspFormatCodeEvent,
insert: newText, })
},
],
})
}
} }
async requestCompletion( async requestCompletion(
@ -320,7 +325,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 +365,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 +384,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 +502,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 +542,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') { lspAutocompleteExt(plugin),
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 './lsp'
export interface SemanticToken {
from: number
to: number
type: string
modifiers: string[]
}
export const addToken = StateEffect.define<SemanticToken>({
map: (token: SemanticToken, change) => ({
...token,
from: change.mapPos(token.from),
to: change.mapPos(token.to),
}),
})
export default function lspSemanticTokenExt(): Extension {
return StateField.define<DecorationSet>({
create() {
return Decoration.none
},
update(highlights, tr) {
// Nothing can come before this line, this is very important!
// It makes sure the highlights are updated correctly for the changes.
highlights = highlights.map(tr.changes)
const isSemanticTokensEvent = tr.annotation(lspSemanticTokensEvent.type)
if (!isSemanticTokensEvent) {
return highlights
}
// Check if any of the changes are addToken
const hasAddToken = tr.effects.some((e) => e.is(addToken))
if (hasAddToken) {
highlights = highlights.update({
filter: (from, to) => false,
})
}
for (const e of tr.effects)
if (e.is(addToken)) {
const tag = getTag(e.value)
const className = tag
? highlightingFor(tr.startState, [tag])
: undefined
if (e.value.from < e.value.to && tag) {
if (className) {
highlights = highlights.update({
add: [
Decoration.mark({ class: className }).range(
e.value.from,
e.value.to
),
],
})
}
}
}
return highlights
},
provide: (f) => EditorView.decorations.from(f),
})
}
export function getTag(semanticToken: SemanticToken): Tag | null {
let tokenType = convertSemanticTokenTypeToCodeMirrorTag(semanticToken.type)
if (
semanticToken.modifiers === undefined ||
semanticToken.modifiers === null ||
semanticToken.modifiers.length === 0
) {
return tokenType
}
for (let modifier of semanticToken.modifiers) {
tokenType = convertSemanticTokenToCodeMirrorTag(
'',
modifier,
tokenType || undefined
)
}
return tokenType
}
export function getTagName(semanticToken: SemanticToken): string {
let tokenType = semanticToken.type
if (
semanticToken.modifiers === undefined ||
semanticToken.modifiers === null ||
semanticToken.modifiers.length === 0
) {
return tokenType
}
for (let modifier of semanticToken.modifiers) {
tokenType = `${tokenType}.${modifier}`
}
return tokenType
}
function convertSemanticTokenTypeToCodeMirrorTag(
tokenType: string
): Tag | null {
switch (tokenType) {
case 'keyword':
return tags.keyword
case 'variable':
return tags.variableName
case 'string':
return tags.string
case 'number':
return tags.number
case 'comment':
return tags.comment
case 'operator':
return tags.operator
case 'function':
return tags.function(tags.name)
case 'type':
return tags.typeName
case 'property':
return tags.propertyName
case 'parameter':
return tags.local(tags.name)
default:
console.error('Unknown token type:', tokenType)
return null
}
}
function convertSemanticTokenToCodeMirrorTag(
tokenType: string,
tokenModifier: string,
givenTag?: Tag
): Tag | null {
let tag = givenTag
? givenTag
: convertSemanticTokenTypeToCodeMirrorTag(tokenType)
if (!tag) {
return null
}
if (tokenModifier) {
switch (tokenModifier) {
case 'definition':
return tags.definition(tag)
case 'declaration':
return tags.definition(tag)
case 'readonly':
return tags.constant(tag)
case 'static':
return tags.constant(tag)
case 'defaultLibrary':
return tags.standard(tag)
default:
console.error('Unknown token modifier:', tokenModifier)
return tag
}
}
return tag
}

View File

@ -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: {},
}, },
} }

545
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

41
src/AppState.tsx Normal file
View File

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

View File

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

View File

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

View File

@ -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

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

View File

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

View File

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

View File

@ -26,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

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,15 @@
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, useMemo, useContext, useState } from 'react'
createContext, import {
useMemo, LanguageServerClient,
useEffect, FromServer,
useContext, IntoServer,
useState, LspWorkerEventType,
} from 'react' LanguageServerPlugin,
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' } from '@kittycad/codemirror-lsp-client'
import Client from '../editor/plugins/lsp/client'
import { TEST, VITE_KC_API_BASE_URL } from 'env' import { TEST, VITE_KC_API_BASE_URL } from 'env'
import 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 { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Extension } from '@codemirror/state' import { Extension } from '@codemirror/state'
import { LanguageSupport } from '@codemirror/language' import { LanguageSupport } from '@codemirror/language'
@ -21,16 +18,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 { NetworkHealthState } from 'hooks/useNetworkStatus'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { isTauri } from 'lib/isTauri'
import { codeManager } from 'lib/singletons'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -70,21 +66,8 @@ type LspContext = {
export const LspStateContext = createContext({} as LspContext) export const LspStateContext = createContext({} as LspContext)
export const LspProvider = ({ children }: { children: React.ReactNode }) => { export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const { const [isKclLspReady, setIsKclLspReady] = useState(false)
isKclLspServerReady, const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
isCopilotLspServerReady,
setIsKclLspServerReady,
setIsCopilotLspServerReady,
isStreamReady,
} = useStore((s) => ({
isKclLspServerReady: s.isKclLspServerReady,
isCopilotLspServerReady: s.isCopilotLspServerReady,
setIsKclLspServerReady: s.setIsKclLspServerReady,
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
isStreamReady: s.isStreamReady,
}))
const [isLspReady, setIsLspReady] = useState(false)
const [isCopilotReady, setIsCopilotReady] = useState(false)
const { const {
auth, auth,
@ -96,8 +79,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 +109,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: () => {
setIsKclLspReady(true)
},
})
setIsLspReady(true)
const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl })
return { lspClient } return { lspClient }
}, [ }, [
// We need a token for authenticating the server. // We need a token for authenticating the server.
token, token,
]) ])
useMemo(() => {
if (!isTauri() && isKclLspReady && kclLspClient && codeManager.code) {
kclLspClient.textDocumentDidOpen({
textDocument: {
uri: `file:///${PROJECT_ENTRYPOINT}`,
languageId: 'kcl',
version: 1,
text: codeManager.code,
},
})
}
}, [kclLspClient, isKclLspReady])
// Here we initialize the plugin which will start the client. // Here we initialize the plugin which will start the client.
// Now that we have multi-file support the name of the file is a dep of // Now that we have multi-file support the name of the file is a dep of
// this use memo, as well as the directory structure, which I think is // this use memo, as well as the directory structure, which I think is
@ -146,39 +144,38 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => { const kclLSP = useMemo(() => {
let plugin = null let plugin = null
if (isKclLspServerReady && !TEST && kclLspClient) { if (isKclLspReady && !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
} }
return plugin return plugin
}, [kclLspClient, isKclLspServerReady]) }, [kclLspClient, isKclLspReady])
// 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) {
@ -205,13 +202,13 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const fromServer: FromServer | Error = FromServer.create() const fromServer: FromServer | Error = FromServer.create()
if (err(fromServer)) return { lspClient: null } if (err(fromServer)) return { lspClient: null }
const client = new Client(fromServer, intoServer)
setIsCopilotReady(true)
const lspClient = new LanguageServerClient({ const lspClient = new LanguageServerClient({
client,
name: LspWorker.Copilot, name: LspWorker.Copilot,
fromServer,
intoServer,
initializedCallback: () => {
setIsCopilotLspReady(true)
},
}) })
return { lspClient } return { lspClient }
}, [token]) }, [token])
@ -223,7 +220,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
const copilotLSP = useMemo(() => { const copilotLSP = useMemo(() => {
let plugin = null let plugin = null
if (isCopilotLspServerReady && !TEST && copilotLspClient) { if (isCopilotLspReady && !TEST && copilotLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = copilotPlugin({ const lsp = copilotPlugin({
documentUri: `file:///${PROJECT_ENTRYPOINT}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,
@ -235,7 +232,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
plugin = lsp plugin = lsp
} }
return plugin return plugin
}, [copilotLspClient, isCopilotLspServerReady]) }, [copilotLspClient, isCopilotLspReady])
let lspClients: LanguageServerClient[] = [] let lspClients: LanguageServerClient[] = []
if (kclLspClient) { if (kclLspClient) {
@ -245,13 +242,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
lspClients.push(copilotLspClient) lspClients.push(copilotLspClient)
} }
useEffect(() => {
setIsKclLspServerReady(isLspReady)
}, [isLspReady])
useEffect(() => {
setIsCopilotLspServerReady(isCopilotReady)
}, [isCopilotReady])
const onProjectClose = ( const onProjectClose = (
file: FileEntry | null, file: FileEntry | null,
projectPath: string | null, projectPath: string | null,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,22 +2,17 @@ import { coreDump } from 'lang/wasm'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import React from 'react' import React, { useMemo } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { useStore } from 'useStore'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
export const RefreshButton = ({ children }: React.PropsWithChildren) => { export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const { htmlRef } = useStore((s) => ({ const coreDumpManager = useMemo(
htmlRef: s.htmlRef, () => new CoreDumpManager(engineCommandManager, token),
})) []
const coreDumpManager = new CoreDumpManager(
engineCommandManager,
htmlRef,
token
) )
async function refresh() { async function refresh() {

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
@ -15,6 +27,7 @@ function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
export default class EditorManager { export default class EditorManager {
private _editorView: EditorView | null = null private _editorView: EditorView | null = null
private _copilotEnabled: boolean = true
private _isShiftDown: boolean = false private _isShiftDown: boolean = false
private _selectionRanges: Selections = { private _selectionRanges: Selections = {
@ -22,8 +35,6 @@ export default class EditorManager {
codeBasedSelections: [], codeBasedSelections: [],
} }
private _lastSelectionEvent: number | null = null
lastSelection: string = ''
private _lastEvent: { event: string; time: number } | null = null private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {} private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
@ -37,6 +48,14 @@ export default class EditorManager {
private _highlightRange: [number, number] = [0, 0] private _highlightRange: [number, number] = [0, 0]
setCopilotEnabled(enabled: boolean) {
this._copilotEnabled = enabled
}
get copilotEnabled(): boolean {
return this._copilotEnabled
}
setEditorView(editorView: EditorView) { setEditorView(editorView: EditorView) {
this._editorView = editorView this._editorView = editorView
} }
@ -57,10 +76,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 +98,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 +144,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,56 +194,35 @@ 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
} }
// note this is also set from the "Set selection" action to stop code mirror from updating selections right after
// selections are made from the scene
this.lastSelection = selString
if ( const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
this._lastSelectionEvent &&
Date.now() - this._lastSelectionEvent < 150
) {
return // update triggered by scene selection
}
if (sceneInfra.selected) {
return // mid drag
}
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
'Equip rectangle tool',
]
if (!this._modelingEvent) { if (!this._modelingEvent) {
return return
@ -266,7 +265,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,37 @@ 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, offsetToPos,
documentUri, posToOffset,
LanguageServerOptions,
LanguageServerClient,
docPathFacet,
languageId, languageId,
workspaceFolders, } from '@kittycad/codemirror-lsp-client'
} from 'editor/plugins/lsp/plugin' 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'
import { editorManager } from 'lib/singletons'
const copilotPluginAnnotation = Annotation.define<null>()
export const copilotPluginEvent = copilotPluginAnnotation.of(null)
const rejectSuggestionAnnotation = Annotation.define<null>()
export const rejectSuggestionCommand = rejectSuggestionAnnotation.of(null)
// Effects to tell StateEffect what to do with GhostText
const addSuggestion = StateEffect.define<Suggestion>()
const acceptSuggestion = StateEffect.define<null>()
const clearSuggestion = StateEffect.define<null>()
const typeFirst = StateEffect.define<number>()
const ghostMark = Decoration.mark({ class: 'cm-ghostText' }) const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
const changesDelay = 600
interface Suggestion { interface Suggestion {
text: string text: string
displayText: string displayText: string
@ -38,15 +63,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 +80,19 @@ 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
}
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 +188,448 @@ export const completionDecoration = StateField.define<CompletionState>({
), ),
}) })
const copilotEvent = Annotation.define<null>() // A view plugin that requests completions from the server after a delay
export class CompletionRequester implements PluginValue {
private client: LanguageServerClient
private lastPos: number = 0
/**************************************************************************** private queuedUids: string[] = []
************************* COMMANDS ******************************************
*****************************************************************************/
const acceptSuggestionCommand = ( private _deffererCodeUpdate = deferExecution(() => {
copilotClient: LanguageServerClient, this.requestCompletions()
view: EditorView }, changesDelay)
) => {
// We delete the ghost text and insert the suggestion. private _deffererUserSelect = deferExecution(() => {
// We also set the cursor to the end of the suggestion. this.rejectSuggestionCommand()
const ghostText = view.state.field(completionDecoration)!.ghostText }, changesDelay)
if (!ghostText) {
return false constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client
} }
const ghostTextStart = ghostText.displayPos update(viewUpdate: ViewUpdate) {
const ghostTextEnd = ghostText.endGhostText // Make sure we are in a state where we can request completions.
if (!editorManager.copilotEnabled) {
return
}
const actualTextStart = ghostText.startPos let isUserSelect = false
const actualTextEnd = ghostText.endPos let isRelevant = false
for (const tr of viewUpdate.transactions) {
if (tr.isUserEvent('select')) {
isUserSelect = true
break
} else if (tr.isUserEvent('input')) {
isRelevant = true
} else if (tr.isUserEvent('delete')) {
isRelevant = true
} else if (tr.isUserEvent('undo')) {
isRelevant = true
} else if (tr.isUserEvent('redo')) {
isRelevant = true
} else if (tr.isUserEvent('move')) {
isRelevant = true
} else if (tr.annotation(copilotPluginEvent.type) !== undefined) {
isRelevant = true
}
}
const replacementEnd = ghostText.endReplacement // If we have a user select event, we want to clear the ghost text.
if (isUserSelect) {
this._deffererUserSelect(true)
return
}
const suggestion = ghostText.text if (!isRelevant) {
return
}
view.dispatch({ this.lastPos = this.view.state.selection.main.head
changes: { if (viewUpdate.docChanged) this._deffererCodeUpdate(true)
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
// selection: {anchor: actualTextEnd},
effects: acceptSuggestion.of(null),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
insert: suggestion,
},
selection: { anchor: actualTextEnd },
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)],
})
copilotClient.accept(ghostText.uuid)
return true
}
export const rejectSuggestionCommand = (
copilotClient: LanguageServerClient,
view: EditorView
) => {
// We delete the suggestion, then carry through with the original keypress
const ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
return false
} }
const ghostTextStart = ghostText.displayPos ghostText(): GhostText | null {
const ghostTextEnd = ghostText.endGhostText return this.view.state.field(completionDecoration)?.ghostText || null
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 = (
copilotClient: LanguageServerClient,
view: EditorView,
key: string
) => {
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
const ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
return false
} }
const ghostTextStart = ghostText.displayPos
const indent = view.state.facet(indentUnit)
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) { containsGhostText(): boolean {
view.dispatch({ return this.ghostText() !== null
selection: { anchor: ghostTextStart + indent.length }, }
effects: typeFirst.of(indent.length),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], autocompleting(): boolean {
}) return completionStatus(this.view.state) === 'active'
return true }
} else if (key === 'Tab') {
return acceptSuggestionCommand(copilotClient, view) notFocused(): boolean {
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) { return !this.view.hasFocus
return rejectSuggestionCommand(copilotClient, view) }
} else if (ghostText.displayText.length === 1) {
return acceptSuggestionCommand(copilotClient, view) async requestCompletions(): Promise<void> {
} else { if (
// Use this to delete the first letter of the suggestion this.containsGhostText() ||
view.dispatch({ this.autocompleting() ||
selection: { anchor: ghostTextStart + 1 }, this.notFocused()
effects: typeFirst.of(1), ) {
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], return
}
const pos = this.view.state.selection.main.head
// Check if the position has changed
if (pos !== this.lastPos) {
return
}
// Get the current position and source
const state = this.view.state
const dUri = state.facet(docPathFacet)
// Request completion from the server
const completionResult = await this.getCompletion({
doc: {
source: state.doc.toString(),
tabSize: state.facet(EditorState.tabSize),
indentSize: 1,
insertSpaces: true,
path: dUri.split('/').pop()!,
uri: dUri,
relativePath: dUri.replace('file://', ''),
languageId: state.facet(languageId),
position: offsetToPos(state.doc, pos),
},
}) })
if (completionResult.completions.length === 0) {
return
}
let {
text,
displayText,
range: { start },
position,
uuid,
} = completionResult.completions[0]
if (text.length === 0 || displayText.length === 0) {
return
}
const startPos = posToOffset(state.doc, {
line: start.line,
character: start.character,
})
if (startPos === undefined) {
return
}
const endGhostOffset = posToOffset(state.doc, {
line: position.line,
character: position.character,
})
if (endGhostOffset === undefined) {
return
}
const endGhostPos = endGhostOffset + displayText.length
// EndPos is the position that marks the complete end
// of what is to be replaced when we accept a completion
// result
const endPos = startPos + text.length
// Check if they changed position.
if (pos !== this.lastPos) {
return
}
// Make sure we are not currently completing.
if (this.autocompleting() || this.notFocused()) {
return
}
// Dispatch an effect to add the suggestion
// If the completion starts before the end of the line, check the end of the line with the end of the completion.
const line = this.view.state.doc.lineAt(pos)
if (line.to !== pos) {
const ending = this.view.state.doc.sliceString(pos, line.to)
if (displayText.endsWith(ending)) {
displayText = displayText.slice(0, displayText.length - ending.length)
} else if (displayText.includes(ending)) {
// Remove the ending
this.view.dispatch({
changes: {
from: pos,
to: line.to,
insert: '',
},
selection: { anchor: pos },
effects: typeFirst.of(ending.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
}
}
this.view.dispatch({
changes: {
from: pos,
to: pos,
insert: displayText,
},
effects: [
addSuggestion.of({
displayText,
endReplacement: endGhostPos,
text,
cursorPos: pos,
startPos,
endPos,
uuid,
}),
],
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
this.lastPos = pos
return
}
acceptSuggestionCommand(): boolean {
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
// We delete the ghost text and insert the suggestion.
// We also set the cursor to the end of the suggestion.
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
const actualTextStart = ghostText.startPos
const actualTextEnd = ghostText.endPos
const replacementEnd = ghostText.endReplacement
const suggestion = ghostText.text
this.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: acceptSuggestion.of(null),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
this.view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
insert: suggestion,
},
selection: { anchor: actualTextEnd },
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
})
this.accept(ghostText.uuid)
return true return true
} }
rejectSuggestionCommand(): boolean {
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
// We delete the suggestion, then carry through with the original keypress
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
this.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: clearSuggestion.of(null),
annotations: [
rejectSuggestionCommand,
Transaction.addToHistory.of(false),
],
})
this.reject()
return false
}
sameKeyCommand(key: string) {
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.view.state.facet(indentUnit)
if (key === tabKey && ghostText.displayText.startsWith(indent)) {
this.view.dispatch({
selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
return true
} else if (key === tabKey) {
return this.acceptSuggestionCommand()
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
return this.rejectSuggestionCommand()
} else if (ghostText.displayText.length === 1) {
return this.acceptSuggestionCommand()
} else {
// Use this to delete the first letter of the suggestion
this.view.dispatch({
selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
return true
}
}
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(view, options.client))
)
const domHandlers = EditorView.domEventHandlers({
keydown(event, view) { keydown(event, view) {
if ( if (
event.key !== 'Shift' && event.key !== 'Shift' &&
event.key !== 'Control' && event.key !== 'Control' &&
event.key !== 'Alt' && event.key !== 'Alt' &&
event.key !== 'Backspace' &&
event.key !== 'Delete' &&
event.key !== 'Meta' event.key !== 'Meta'
) { ) {
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 rejectSuggestionCommand = (view: EditorView): boolean => {
// Get the current plugin from the map.
const p = view.plugin(completionPlugin)
if (p === null) return false
return p.rejectSuggestionCommand()
}
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: rejectSuggestionCommand,
},
{
key: 'Delete',
run: rejectSuggestionCommand,
},
{ key: 'Mod-z', run: rejectSuggestionCommand, preventDefault: true },
{
key: 'Mod-y',
mac: 'Mod-Shift-z',
run: rejectSuggestionCommand,
preventDefault: true,
},
{
linux: 'Ctrl-Shift-z',
run: rejectSuggestionCommand,
preventDefault: true,
},
{ key: 'Mod-u', run: rejectSuggestionCommand, preventDefault: true },
{
key: 'Alt-u',
mac: 'Mod-Shift-u',
run: rejectSuggestionCommand,
preventDefault: true,
},
]
const viewCompletionPlugin = (copilotClient: LanguageServerClient) => const copilotAutocompleteKeymapExt = Prec.highest(
EditorView.updateListener.of((update) => { keymap.computeN([], () => [copilotAutocompleteKeymap])
if (update.focusChanged) { )
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) => {
for (const tr of update.transactions) {
if (tr.annotation(copilotEvent) !== undefined) {
return true
}
}
return false
}
const containsGhostText = (update: ViewUpdate) => {
return update.state.field(completionDecoration).ghostText != null
}
const autocompleting = (update: ViewUpdate) => {
return completionStatus(update.state) === 'active'
}
const notFocused = (update: ViewUpdate) => {
return !update.view.hasFocus
}
return EditorView.updateListener.of((update: ViewUpdate) => {
if (
update.docChanged &&
!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
const state = update.state
const pos = state.selection.main.head
const source = state.doc.toString()
const dUri = state.facet(documentUri)
const path = dUri.split('/').pop()!
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), completionPlugin,
languageId.of('kcl'), copilotAutocompleteKeymapExt,
workspaceFolders.of(options.workspaceFolders), domHandlers,
ViewPlugin.define(
(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,108 @@
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, LanguageServerClient,
EditorView, lspPlugin,
keymap, lspFormatCodeEvent,
KeyBinding, } from '@kittycad/codemirror-lsp-client'
tooltips, import { deferExecution } from 'lib/utils'
} from '@codemirror/view' import { codeManager, editorManager, kclManager } from 'lib/singletons'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol' import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams'
import { offsetToPos } from 'editor/plugins/lsp/util' import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams'
import { LanguageServerOptions } from 'editor/plugins/lsp' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
import { syntaxTree, indentService, foldService } from '@codemirror/language' import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
import {
LanguageServerPlugin,
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) => { // A view plugin that requests completions from the server after a delay
try { export class KclPlugin implements PluginValue {
const previousLine = context.lineAt(pos, -1) private viewUpdate: ViewUpdate | null = null
const previousLineText = previousLine.text.replaceAll( private client: LanguageServerClient
'\t',
' '.repeat(context.state.tabSize) constructor(client: LanguageServerClient) {
) this.client = client
const match = previousLineText.match(/^(\s)*/) }
if (match === null || match.length <= 0) return null
return match[0].length private _deffererCodeUpdate = deferExecution(() => {
} catch (err) { if (this.viewUpdate === null) {
console.error('Error in codemirror indentService', err) return
} }
return null
}) kclManager.executeCode()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
editorManager.handleOnViewUpdate(this.viewUpdate)
}, 50)
update(viewUpdate: ViewUpdate) {
this.viewUpdate = viewUpdate
editorManager.setEditorView(viewUpdate.view)
let isUserSelect = false
let isRelevant = false
for (const tr of viewUpdate.transactions) {
if (tr.isUserEvent('select')) {
isUserSelect = true
break
} else if (tr.isUserEvent('input')) {
isRelevant = true
} else if (tr.isUserEvent('delete')) {
isRelevant = true
} else if (tr.isUserEvent('undo')) {
isRelevant = true
} else if (tr.isUserEvent('redo')) {
isRelevant = true
} else if (tr.isUserEvent('move')) {
isRelevant = true
} else if (tr.annotation(lspFormatCodeEvent.type)) {
isRelevant = true
}
}
// If we have a user select event, we want to update what parts are
// highlighted.
if (isUserSelect) {
this._deffererUserSelect(true)
return
}
if (!isRelevant) {
return
}
if (!viewUpdate.docChanged) {
return
}
const newCode = viewUpdate.state.doc.toString()
codeManager.code = newCode
codeManager.writeToFile()
this._deffererCodeUpdate(true)
}
async updateUnits(
params: UpdateUnitsParams
): Promise<UpdateUnitsResponse | null> {
return this.client.requestCustom('kcl/updateUnits', params)
}
async updateCanExecute(
params: UpdateCanExecuteParams
): Promise<UpdateCanExecuteResponse> {
return this.client.requestCustom('kcl/updateCanExecute', params)
}
} }
export function kclPlugin(options: LanguageServerOptions): Extension { export function kclPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null
const viewPlugin = ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
)
const kclKeymap: readonly KeyBinding[] = [
{
key: 'Alt-Shift-f',
run: (view: EditorView) => {
if (view.plugin === null) return false
// Get the current plugin from the map.
const p = view.plugin(viewPlugin)
if (p === null) return false
p.requestFormatting()
return true
},
},
]
// Create an extension for the key mappings.
const kclKeymapExt = Prec.highest(keymap.computeN([], () => [kclKeymap]))
const folding = foldService.of(
(state: EditorState, lineStart: number, lineEnd: number) => {
if (plugin == null) return null
// Get the folding ranges from the language server.
// Since this is async we directly need to update the folding ranges after.
return plugin?.foldingRange(lineStart, lineEnd)
}
)
return [ return [
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,19 @@ class KclLanguage extends Language {
workspaceFolders: options.workspaceFolders, workspaceFolders: options.workspaceFolders,
allowHTMLContent: true, allowHTMLContent: true,
client: options.client, client: options.client,
processLspNotification: options.processLspNotification,
}) })
super( const parser = new KclParser()
data,
// For now let's use the javascript parser. super(data, parser, [plugin], 'kcl')
// It works really well and has good syntax highlighting.
// We can use our lsp for the rest.
jsParser,
[
plugin,
EditorState.languageData.of(() => [
{
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
},
]),
],
'kcl'
)
} }
} }
export default 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

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

View File

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

View File

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

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