Compare commits

..

53 Commits

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

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

* disable line tool if rectangle has started

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

* add test

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

* trigger ci

---------

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

* make it robust

* test hardening

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

* trigger CI

* harden test

---------

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

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

* Trigger CI

* Remove promises from getOsInfo for tauri apis

---------

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

"click plane to sketch on"

* Add margin and make the message text smaller

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

---------

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

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

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

* fmt

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

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

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

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

* updates

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

* change theme

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

* updates

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

* updates

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

* updates

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

* disable copilot temporarily

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

* fixes

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

* fixes

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

---------

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

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

* Dispatch only a single transaction in requestFormatting

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

* Remove old comment and some useless tests

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

* small codemirror changes from  marijnh

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

* fix some flaky tests

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

* fix

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Marijn Haverbeke <marijn@haverbeke.berlin>
2024-07-03 19:28:46 -07:00
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
159 changed files with 7402 additions and 5050 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

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]) => {

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;
}

View File

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

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