Compare commits

...

15 Commits

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

* Fix up setting persistence, move under modeling

* Make the setting actually do something

* the lamest fmt I've seen in a while

* Fix clippy warnings

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

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

* Re-run CI after new screenshots added

---------

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

* Adjust sketch mode zoom

* Do not retry failures

* typo

* use sha as file upload id

* again

* again

* again

* again

* Fix camera moving too

* Use virtual fps instead of buffering for mouse

---------

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

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

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

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

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

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

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

* add back in to js side;

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

* order of operations

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

* fxi

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

* typos

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

---------

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

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

* things

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

* updates

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

* fix up most tests

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

* fixups

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

* updates

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

* fixes

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

* fix lints

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

* updates

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

* typo

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

---------

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

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

* typos

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

---------

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

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

tsc passing

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

fixes

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

fixes

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

working

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

fixups

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

updates

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

fixes

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

fixes

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

fmt

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

* cleanups

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

* fixes

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

* udpates

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

* updates

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

* cleanup

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

* cleanup

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

* fixes

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

* updates

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

---------

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

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

* updates

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

---------

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

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

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

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

updates

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

updates

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

fixes

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

updates

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

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

udates

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

fixes

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

updates

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

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

updates

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

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

updates

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

fixes

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

more cleaniup

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

updates

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

dont react to non relevant events

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

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

faster

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

cleanup code

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

defer

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

more events

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

fixes

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

updates

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

updates

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

updates

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

updates

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

cleanup

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

user events

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

updates

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

udpates

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

updates
;

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

upfates

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

updates

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

make highlighting code blocks easier

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

improvements

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

updates

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

updates

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

cleanup

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

updates

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

better builds

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

remove weird hacks

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

updates

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

better checks

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

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

make release builds in prod (#2839)

Update package.json

udpates

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

updates

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

fix some tests

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

updates

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

updates

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

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

updates

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

better timing

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

tweak delay

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

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

upfates

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

fixes

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

fixes

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

updates

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

updates

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

updates

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

ifxup

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

udpates

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

udpates

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

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

udpates

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

wait for the lsp for all screenshots so consistent

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

better playwright

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

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

fixes

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

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

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

better keymaps

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

emptu in comment

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

fix logs

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

fxes

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

add a test for tab to autocomplete

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

updates

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

better

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

printl

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

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

* updates

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

* updates

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

* upfates

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

* cleanup

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

* updates

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

* updates

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

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

* empty

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-29 18:10:07 -07:00
102 changed files with 3525 additions and 1861 deletions

View File

@ -238,12 +238,8 @@ jobs:
shell: cmd
- name: Build the app (debug)
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'false' }}
with:
includeRelease: false
includeDebug: true
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- name: Build for Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
@ -336,7 +332,6 @@ jobs:
# specific and we want to overwrite it with the this new build after and
# not upload the apple store build to the public bucket
- name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
@ -348,8 +343,7 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
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' }}"
with:
args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
@ -367,7 +361,7 @@ jobs:
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri
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 }}
- name: Run e2e tests (windows only)
@ -376,7 +370,7 @@ jobs:
cargo install tauri-driver --force
yarn wdio run wdio.conf.ts
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 }}
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
E2E_TAURI_ENABLED: true

2
.gitignore vendored
View File

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

View File

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

View File

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -207,6 +207,23 @@ export const getMovementUtils = (opts: any) => {
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) {
// Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name()
@ -214,7 +231,7 @@ export async function getUtils(page: Page) {
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
return {
waitForAuthSkipAppStart: () => waitForPageLoad(page),
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
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",
"version": "0.22.6",
"version": "0.22.7",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.16.0",
"@codemirror/autocomplete": "^6.16.3",
"@codemirror/commands": "^6.6.0",
"@codemirror/language": "^6.10.2",
"@codemirror/lint": "^6.8.1",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@csstools/postcss-oklab-function": "^3.0.16",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.67",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@kittycad/lib": "^0.0.69",
"@react-hook/resize-observer": "^2.0.1",
"@replit/codemirror-interact": "^6.3.1",
"@tauri-apps/api": "2.0.0-beta.12",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.3",
"@tauri-apps/plugin-fs": "^2.0.0-beta.5",
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
"@tauri-apps/plugin-os": "^2.0.0-beta.3",
"@tauri-apps/plugin-process": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@tauri-apps/plugin-updater": "^2.0.0-beta.3",
"@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",
"@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/react": "^3.2.2",
"crypto-js": "^4.2.0",
"debounce-promise": "^3.1.2",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"formik": "^2.4.6",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.4.3",
"http-server": "^14.1.1",
"html2canvas-pro": "^1.5.0",
"json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1",
"node-fetch": "^3.3.2",
"re-resizable": "^6.9.11",
"react": "^18.3.1",
"react-dom": "^18.2.0",
@ -55,18 +48,14 @@
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.23.1",
"sketch-helpers": "^0.0.4",
"swr": "^2.2.5",
"three": "^0.164.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"ua-parser-js": "^1.0.37",
"uuid": "^9.0.1",
"vitest": "^1.6.0",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"wasm-pack": "^0.12.1",
"vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2",
"ws": "^8.17.0",
"xstate": "^4.38.2",
"zustand": "^4.5.2"
},
@ -85,8 +74,8 @@
"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": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"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": "(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",
@ -122,12 +111,15 @@
"@babel/preset-env": "^7.24.3",
"@iarna/toml": "^2.2.5",
"@playwright/test": "^1.44.1",
"@tauri-apps/cli": "^2.0.0-beta.13",
"@types/crypto-js": "^4.2.2",
"@types/debounce-promise": "^3.1.9",
"@tauri-apps/cli": "==2.0.0-beta.13",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/mocha": "^10.0.6",
"@types/node": "^18.19.31",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.2.25",
"@types/react-modal": "^3.16.3",
"@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39",
@ -147,21 +139,27 @@
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10",
"http-server": "^14.1.1",
"husky": "^9.0.11",
"node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.31",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.0",
"prettier": "^2.8.8",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1",
"vite": "^5.2.9",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0",
"wasm-pack": "^0.12.1",
"ws": "^8.17.0",
"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 { Codec } from '.'
import Bytes from './bytes'
import PromiseMap from './map'
import Queue from './queue'
import Tracer from '../tracer'
import { Codec } from '../codec'
import Tracer from './tracer'
import PromiseMap from './map'
export default class StreamDemuxer extends Queue<Uint8Array> {
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
@ -15,9 +15,12 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
new Queue<vsrpc.RequestMessage>()
readonly #start: Promise<void>
private trace: boolean = false
constructor() {
constructor(trace?: boolean) {
super()
this.trace = trace || false
this.#start = this.start()
}
@ -64,7 +67,10 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
contentLength = null
const message = JSON.parse(delimited) as vsrpc.Message
Tracer.server(message)
if (this.trace) {
Tracer.server(message)
}
// demux the message stream
if (vsrpc.Message.isResponse(message) && null != message.id) {
@ -85,7 +91,9 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
add(bytes: Uint8Array): void {
const message = Codec.decode(bytes) as vsrpc.Message
Tracer.server(message)
if (this.trace) {
Tracer.server(message)
}
// demux the message stream
if (vsrpc.Message.isResponse(message) && null != message.id) {

View File

@ -1,12 +1,16 @@
import * as jsrpc from 'json-rpc-2.0'
import * as vsrpc from 'vscode-jsonrpc'
import Bytes from './codec/bytes'
import StreamDemuxer from './codec/demuxer'
import Headers from './codec/headers'
import Queue from './codec/queue'
import Bytes from './bytes'
import StreamDemuxer from './demuxer'
import Headers from './headers'
import Queue from './queue'
import Tracer from './tracer'
import { LspWorkerEventType, LspWorker } from './types'
export enum LspWorkerEventType {
Init = 'init',
Call = 'call',
}
export const encoder = new TextEncoder()
export const decoder = new TextDecoder()
@ -33,16 +37,24 @@ export class IntoServer
implements AsyncGenerator<Uint8Array, never, void>
{
private worker: Worker | null = null
private type_: LspWorker | null = null
constructor(type_?: LspWorker, worker?: Worker) {
private type_: String | null = null
private trace: boolean = false
constructor(type_?: String, worker?: Worker, trace?: boolean) {
super()
if (worker && type_) {
this.worker = worker
this.type_ = type_
}
this.trace = trace || false
}
enqueue(item: Uint8Array): void {
Tracer.client(Headers.remove(decoder.decode(item)))
if (this.trace) {
Tracer.client(Headers.remove(decoder.decode(item)))
}
if (this.worker) {
this.worker.postMessage({
worker: this.type_,
@ -71,7 +83,7 @@ export namespace FromServer {
// Calls private method .start() which can throw.
// This is an odd one of the bunch but try/catch seems most suitable here.
try {
return new StreamDemuxer()
return new StreamDemuxer(false)
} catch (e: any) {
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 Client from './client'
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
import { 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'
import { FromServer, IntoServer } from './codec'
import Client from './jsonrpc'
import { LanguageServerPlugin } from '../plugin/lsp'
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
@ -31,12 +23,6 @@ interface LSPRequestMap {
LSP.TextEdit[] | null
]
'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]]
'copilot/getCompletions': [
CopilotLspCompletionParams,
CopilotCompletionResponse
]
'kcl/updateUnits': [UpdateUnitsParams, UpdateUnitsResponse | null]
'kcl/updateCanExecute': [UpdateCanExecuteParams, UpdateCanExecuteResponse]
}
// Client to server
@ -49,21 +35,13 @@ interface LSPNotifyMap {
'workspace/didCreateFiles': LSP.CreateFilesParams
'workspace/didRenameFiles': LSP.RenameFilesParams
'workspace/didDeleteFiles': LSP.DeleteFilesParams
'copilot/notifyAccepted': CopilotAcceptCompletionParams
'copilot/notifyRejected': CopilotRejectCompletionParams
}
export interface LanguageServerClientOptions {
client: Client
name: LspWorker
}
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
name: string
fromServer: FromServer
intoServer: IntoServer
initializedCallback: () => void
}
export class LanguageServerClient {
@ -76,18 +54,18 @@ export class LanguageServerClient {
public initializePromise: Promise<void>
private isUpdatingSemanticTokens: boolean = false
private semanticTokens: SemanticToken[] = []
private queuedUids: string[] = []
constructor(options: LanguageServerClientOptions) {
this.plugins = []
this.client = options.client
this.name = options.name
this.plugins = []
this.client = new Client(
options.fromServer,
options.intoServer,
options.initializedCallback
)
this.ready = false
this.queuedUids = []
this.initializePromise = this.initialize()
}
@ -111,19 +89,10 @@ export class LanguageServerClient {
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
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) {
this.notify('textDocument/didChange', params)
this.updateSemanticTokens(params.textDocument.uri)
}
textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) {
@ -134,18 +103,9 @@ export class LanguageServerClient {
added: 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', {
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) {
@ -160,33 +120,13 @@ export class LanguageServerClient {
this.notify('workspace/didDeleteFiles', params)
}
async updateSemanticTokens(uri: string) {
async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) {
return
}
// Make sure we can only run, if we aren't already running.
if (!this.isUpdatingSemanticTokens) {
this.isUpdatingSemanticTokens = true
const result = await this.request('textDocument/semanticTokens/full', {
textDocument: {
uri,
},
})
this.semanticTokens = await deserializeTokens(
result.data,
this.getServerCapabilities().semanticTokensProvider
)
this.isUpdatingSemanticTokens = false
}
}
getSemanticTokens(): SemanticToken[] {
return this.semanticTokens
return this.request('textDocument/semanticTokens/full', params)
}
async textDocumentHover(params: LSP.HoverParams) {
@ -239,6 +179,10 @@ export class LanguageServerClient {
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>(
method: K,
params: LSPNotifyMap[K]
@ -246,44 +190,8 @@ export class LanguageServerClient {
return this.client.notify(method, params)
}
async getCompletion(params: CopilotLspCompletionParams) {
const response = await this.request('copilot/getCompletions', params)
//
this.queuedUids = [...response.completions.map((c) => c.uuid)]
return response
}
async accept(uuid: string) {
const badUids = this.queuedUids.filter((u) => u !== uuid)
this.queuedUids = []
this.acceptCompletion({ uuid })
this.rejectCompletions({ uuids: badUids })
}
async reject() {
const badUids = this.queuedUids
this.queuedUids = []
this.rejectCompletions({ uuids: badUids })
}
acceptCompletion(params: CopilotAcceptCompletionParams) {
this.notify('copilot/notifyAccepted', params)
}
rejectCompletions(params: CopilotRejectCompletionParams) {
this.notify('copilot/notifyRejected', params)
}
async updateUnits(
params: UpdateUnitsParams
): Promise<UpdateUnitsResponse | null> {
return await this.request('kcl/updateUnits', params)
}
async updateCanExecute(
params: UpdateCanExecuteParams
): Promise<UpdateCanExecuteResponse> {
return await this.request('kcl/updateCanExecute', params)
notifyCustom<P>(method: string, params: P): void {
return this.client.notify(method, params)
}
private processNotifications(notification: LSP.NotificationMessage) {

View File

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

View File

@ -0,0 +1,113 @@
import { autocompletion } from '@codemirror/autocomplete'
import { foldService, syntaxTree } from '@codemirror/language'
import { Extension, EditorState } from '@codemirror/state'
import { ViewPlugin } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import {
docPathFacet,
LanguageServerPlugin,
LanguageServerPluginSpec,
languageId,
workspaceFolders,
LanguageServerOptions,
} from './plugin/lsp'
import { offsetToPos } from './plugin/util'
export type { LanguageServerClientOptions } from './client'
export { LanguageServerClient } from './client'
export {
Codec,
FromServer,
IntoServer,
LspWorkerEventType,
} from './client/codec'
export type { LanguageServerOptions } from './plugin/lsp'
export type { TransactionInfo, RelevantUpdate } from './plugin/annotations'
export { updateInfo, TransactionAnnotation } from './plugin/annotations'
export {
LanguageServerPlugin,
LanguageServerPluginSpec,
docPathFacet,
languageId,
workspaceFolders,
} from './plugin/lsp'
export { posToOffset, offsetToPos } from './plugin/util'
export function lspPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null
const viewPlugin = ViewPlugin.define(
(view) => (plugin = new LanguageServerPlugin(options, view)),
new LanguageServerPluginSpec()
)
let ext = [
docPathFacet.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
viewPlugin,
foldService.of((state: EditorState, lineStart: number, lineEnd: number) => {
if (plugin == null) return null
// Get the folding ranges from the language server.
// Since this is async we directly need to update the folding ranges after.
return plugin?.foldingRange(lineStart, lineEnd)
}),
]
if (options.client.getServerCapabilities().completionProvider) {
ext.push(
autocompletion({
defaultKeymap: false,
override: [
async (context) => {
if (plugin === null) {
return null
}
const { state, pos, explicit } = context
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
})
)
}
return ext
}

View File

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

View File

@ -0,0 +1,51 @@
import {
acceptCompletion,
clearSnippet,
closeCompletion,
hasNextSnippetField,
moveCompletionSelection,
nextSnippetField,
prevSnippetField,
startCompletion,
} from '@codemirror/autocomplete'
import { Prec } from '@codemirror/state'
import { EditorView, keymap, KeyBinding } from '@codemirror/view'
import { CompletionItemKind } from 'vscode-languageserver-protocol'
export const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
) as Record<CompletionItemKind, string>
const lspAutocompleteKeymap: readonly KeyBinding[] = [
{ key: 'Ctrl-Space', run: startCompletion },
{
key: 'Escape',
run: (view: EditorView): boolean => {
if (clearSnippet(view)) return true
return closeCompletion(view)
},
},
{ key: 'ArrowDown', run: moveCompletionSelection(true) },
{ key: 'ArrowUp', run: moveCompletionSelection(false) },
{ key: 'PageDown', run: moveCompletionSelection(true, 'page') },
{ key: 'PageUp', run: moveCompletionSelection(false, 'page') },
{ key: 'Enter', run: acceptCompletion },
{
key: 'Tab',
run: (view: EditorView): boolean => {
if (hasNextSnippetField(view.state)) {
const result = nextSnippetField(view)
return result
}
return acceptCompletion(view)
},
shift: prevSnippetField,
},
]
export const lspAutocompleteKeymapExt = Prec.highest(
keymap.computeN([], () => [lspAutocompleteKeymap])
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

160
src-tauri/Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -74,6 +74,9 @@ export class CameraControls {
enableRotate = true
enablePan = true
enableZoom = true
zoomDataFromLastFrame?: number = undefined
// holds coordinates, and interaction
moveDataFromLastFrame?: [number, number, string] = undefined
lastPerspectiveFov: number = 45
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
@ -101,16 +104,12 @@ export class CameraControls {
get isPerspective() {
return this.camera instanceof PerspectiveCamera
}
private debounceTimer = 0
handleStart = () => {
if (this.debounceTimer) clearTimeout(this.debounceTimer)
this._isCamMovingCallback(true, false)
}
handleEnd = () => {
this.debounceTimer = setTimeout(() => {
this._isCamMovingCallback(false, false)
}, 400) as any as number
this._isCamMovingCallback(false, false)
}
setCam = (camProps: ReactCameraProperties) => {
@ -230,6 +229,7 @@ export class CameraControls {
camSettings.orientation.z,
camSettings.orientation.w
).invert()
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
this.useOrthographicCamera()
@ -258,6 +258,48 @@ export class CameraControls {
}
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(() => {
this.engineCommandManager.subscribeTo({
event: 'camera_drag_end',
@ -342,15 +384,7 @@ export class CameraControls {
if (interaction === 'none') return
if (this.syncDirection === 'engineToClient') {
this.throttledEngCmd({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction,
window: { x: event.clientX, y: event.clientY },
},
cmd_id: uuidv4(),
})
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
return
}
@ -398,34 +432,19 @@ export class CameraControls {
}
onMouseWheel = (event: WheelEvent) => {
// Assume trackpad if the deltas are small and integers
this.handleStart()
if (this.syncDirection === 'engineToClient') {
const interactions = this.interactionGuards.zoom.scrollCallback(
event as any
)
if (!interactions) {
this.handleEnd()
return
}
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: -event.deltaY * 0.4,
},
cmd_id: uuidv4(),
})
this.handleEnd()
this.zoomDataFromLastFrame = event.deltaY
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
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + event.deltaY * 0.01
this.handleStart()
this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001
this.handleEnd()
}

View File

@ -568,6 +568,7 @@ export class SceneEntities {
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride, sketchGroup } =
await this.setupSketch({
sketchPathToNode,

View File

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

View File

@ -1,4 +1,3 @@
import { LanguageServerClient } from 'editor/plugins/lsp'
import type * as LSP from 'vscode-languageserver-protocol'
import React, {
createContext,
@ -7,10 +6,15 @@ import React, {
useContext,
useState,
} from 'react'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Client from '../editor/plugins/lsp/client'
import {
LanguageServerClient,
FromServer,
IntoServer,
LspWorkerEventType,
LanguageServerPlugin,
} from '@kittycad/codemirror-lsp-client'
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 { useStore } from 'useStore'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -21,16 +25,15 @@ import { paths } from 'lib/paths'
import { FileEntry } from 'lib/types'
import Worker from 'editor/plugins/lsp/worker.ts?worker'
import {
LspWorkerEventType,
KclWorkerOptions,
CopilotWorkerOptions,
LspWorker,
} from 'editor/plugins/lsp/types'
import { wasmUrl } from 'lang/wasm'
import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { err } from 'lib/trap'
import { isTauri } from 'lib/isTauri'
import { codeManager } from 'lib/singletons'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return []
@ -75,13 +78,11 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
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)
@ -96,8 +97,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
} = useSettingsAuthContext()
const token = auth?.context.token
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.
// But the server happens async so we break this into two parts.
@ -128,17 +127,34 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const fromServer: FromServer | Error = FromServer.create()
if (err(fromServer)) return { lspClient: null }
const client = new Client(fromServer, intoServer)
const lspClient = new LanguageServerClient({
name: LspWorker.Kcl,
fromServer,
intoServer,
initializedCallback: () => {
setIsLspReady(true)
},
})
setIsLspReady(true)
const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl })
return { lspClient }
}, [
// We need a token for authenticating the server.
token,
])
useMemo(() => {
if (!isTauri() && isKclLspServerReady && kclLspClient && codeManager.code) {
kclLspClient.textDocumentDidOpen({
textDocument: {
uri: `file:///${PROJECT_ENTRYPOINT}`,
languageId: 'kcl',
version: 1,
text: codeManager.code,
},
})
}
}, [kclLspClient, isKclLspServerReady])
// Here we initialize the plugin which will start the client.
// 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
@ -148,10 +164,30 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
let plugin = null
if (isKclLspServerReady && !TEST && kclLspClient) {
// Set up the lsp plugin.
const lsp = kclLanguage({
const lsp = new KclLanguageSupport({
documentUri: `file:///${PROJECT_ENTRYPOINT}`,
workspaceFolders: getWorkspaceFolders(),
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
@ -159,27 +195,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
return plugin
}, [kclLspClient, isKclLspServerReady])
// Re-execute the scene when the units change.
useEffect(() => {
if (kclLspClient) {
let plugins = kclLspClient.plugins
for (let plugin of plugins) {
if (plugin.updateUnits && isStreamReady && isNetworkOkay) {
plugin.updateUnits(defaultUnit.current)
}
}
}
}, [
kclLspClient,
defaultUnit.current,
// We want to re-execute the scene if the network comes back online.
// The lsp server will only re-execute if there were previous errors or
// changes, so it's fine to send it thru here.
isStreamReady,
isNetworkOkay,
])
const { lspClient: copilotLspClient } = useMemo(() => {
if (!token || token === '' || TEST) {
return { lspClient: null }
@ -205,13 +220,13 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const fromServer: FromServer | Error = FromServer.create()
if (err(fromServer)) return { lspClient: null }
const client = new Client(fromServer, intoServer)
setIsCopilotReady(true)
const lspClient = new LanguageServerClient({
client,
name: LspWorker.Copilot,
fromServer,
intoServer,
initializedCallback: () => {
setIsCopilotReady(true)
},
})
return { lspClient }
}, [token])

View File

@ -71,7 +71,7 @@ import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror'
import { EditorSelection, Transaction } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump'
import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
@ -80,6 +80,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -101,7 +102,7 @@ export const ModelingMachineProvider = ({
settings: {
context: {
app: { theme, enableSSAO },
modeling: { defaultUnit, highlightEdges },
modeling: { defaultUnit, highlightEdges, showScaleGrid },
},
},
} = useSettingsAuthContext()
@ -116,6 +117,7 @@ export const ModelingMachineProvider = ({
theme: theme.current,
highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current,
})
const { htmlRef } = useStore((s) => ({
htmlRef: s.htmlRef,
@ -281,11 +283,15 @@ export const ModelingMachineProvider = ({
const dispatchSelection = (selection?: EditorSelection) => {
if (!selection) return // TODO less of hack for the below please
if (!editorManager.editorView) return
editorManager.lastSelectionEvent = Date.now()
setTimeout(() => {
if (editorManager.editorView) {
editorManager.editorView.dispatch({ selection })
}
if (!editorManager.editorView) return
editorManager.editorView.dispatch({
selection,
annotations: [
modelingMachineEvent,
Transaction.addToHistory.of(false),
],
})
})
}
let selections: Selections = {
@ -328,11 +334,6 @@ export const ModelingMachineProvider = ({
)
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 {
selectionRanges: selections,
}

View File

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

View File

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

View File

@ -134,6 +134,11 @@ export const SettingsAuthProviderBase = ({
},
})
},
setEngineScaleGridVisibility: (context) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: (context) => {
const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme

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'
export { EditorView }
export const addLineHighlight = StateEffect.define<[number, number]>()
const addLineHighlightAnnotation = Annotation.define<null>()
export const addLineHighlightEvent = addLineHighlightAnnotation.of(null)
export const lineHighlightField = StateField.define({
create() {
return Decoration.none
},
update(lines, tr) {
lines = lines.map(tr.changes)
const isLineHighlightEvent = tr.annotation(addLineHighlightEvent.type)
if (isLineHighlightEvent === undefined) {
return lines
}
const deco = []
for (let e of tr.effects) {
if (e.is(addLineHighlight)) {

View File

@ -1,13 +1,25 @@
import { hasNextSnippetField } from '@codemirror/autocomplete'
import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, SelectionRange } from '@codemirror/state'
import { engineCommandManager, sceneInfra } from 'lib/singletons'
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight } from './highlightextension'
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
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 {
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
@ -22,8 +34,6 @@ export default class EditorManager {
codeBasedSelections: [],
}
private _lastSelectionEvent: number | null = null
lastSelection: string = ''
private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
@ -57,10 +67,6 @@ export default class EditorManager {
this._selectionRanges = selectionRanges
}
set lastSelectionEvent(time: number) {
this._lastSelectionEvent = time
}
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
this._modelingSend = send
}
@ -83,32 +89,39 @@ export default class EditorManager {
setHighlightRange(selection: Selection['range']): void {
this._highlightRange = selection
const editorView = this.editorView
const safeEnd = Math.min(
selection[1],
editorView?.state.doc.length || selection[1]
this._editorView?.state.doc.length || selection[1]
)
if (editorView) {
editorView.dispatch({
if (this._editorView) {
this._editorView.dispatch({
effects: addLineHighlight.of([selection[0], safeEnd]),
annotations: [
updateOutsideEditorEvent,
addLineHighlightEvent,
Transaction.addToHistory.of(false),
],
})
}
}
clearDiagnostics(): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
this.setDiagnostics([])
}
setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
if (!this._editorView) return
this._editorView.dispatch({
effects: [setDiagnosticsEffect.of(diagnostics)],
annotations: [setDiagnosticsEvent, Transaction.addToHistory.of(false)],
})
}
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)
})
@ -122,9 +135,7 @@ export default class EditorManager {
uniqueDiagnostics.add(diagnostic)
})
this.editorView.dispatch(
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
)
this.setDiagnostics([...uniqueDiagnostics])
}
undo() {
@ -174,50 +185,33 @@ export default class EditorManager {
].range[1]
)
)
if (!this.editorView) {
if (!this._editorView) {
return
}
this.editorView.dispatch({
this._editorView.dispatch({
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 {
// If we are just fucking around in a snippet, return early and don't
// 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) {
if (!this._editorView) {
this.setEditorView(viewUpdate.view)
}
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === this.lastSelection) {
// onUpdate is noisy and is fired a lot by extensions
// since we're only interested in selections changes we can ignore most of these.
const ranges = viewUpdate?.state?.selection?.ranges || []
if (ranges.length === 0) {
return
}
// note this is also set from the "Set selection" action to stop code mirror from updating selections right after
// selections are made from the scene
this.lastSelection = selString
if (
this._lastSelectionEvent &&
Date.now() - this._lastSelectionEvent < 150
) {
return // update triggered by scene selection
}
if (sceneInfra.selected) {
return // mid drag
}
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
@ -266,7 +260,3 @@ export default class EditorManager {
)
}
}
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -1,11 +1,16 @@
/// Thanks to the Cursor folks for their heavy lifting here.
/// This has been heavily modified from their original implementation but we are
/// still grateful.
import { indentUnit } from '@codemirror/language'
import {
Decoration,
DecorationSet,
EditorView,
KeyBinding,
PluginValue,
ViewPlugin,
ViewUpdate,
keymap,
} from '@codemirror/view'
import {
Annotation,
@ -17,17 +22,41 @@ import {
Transaction,
} from '@codemirror/state'
import { completionStatus } from '@codemirror/autocomplete'
import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
import {
LanguageServerPlugin,
documentUri,
TransactionAnnotation,
offsetToPos,
posToOffset,
LanguageServerOptions,
LanguageServerClient,
docPathFacet,
languageId,
workspaceFolders,
} from 'editor/plugins/lsp/plugin'
TransactionInfo,
updateInfo,
RelevantUpdate,
lspPlugin,
} from '@kittycad/codemirror-lsp-client'
import { deferExecution } from 'lib/utils'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
const copilotPluginAnnotation = Annotation.define<null>()
export const copilotPluginEvent = copilotPluginAnnotation.of(null)
const rejectSuggestionAnnotation = Annotation.define<null>()
export const rejectSuggestionCommand = rejectSuggestionAnnotation.of(null)
// Effects to tell StateEffect what to do with GhostText
const addSuggestion = StateEffect.define<Suggestion>()
const acceptSuggestion = StateEffect.define<null>()
const clearSuggestion = StateEffect.define<null>()
const typeFirst = StateEffect.define<number>()
const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
const changesDelay = 600
interface Suggestion {
text: string
displayText: string
@ -38,15 +67,10 @@ interface Suggestion {
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 {
ghostText: GhostText | null
}
interface GhostText {
text: string
displayText: string
@ -60,11 +84,24 @@ interface GhostText {
uuid: string
}
export const completionDecoration = StateField.define<CompletionState>({
const completionDecoration = StateField.define<CompletionState>({
create(_state: EditorState) {
return { ghostText: null }
},
update(state: CompletionState, transaction: Transaction) {
// We only care about events from this plugin.
if (
transaction.annotation(copilotPluginEvent.type) === undefined &&
transaction.annotation(rejectSuggestionCommand.type) === undefined
) {
return state
}
// We only care about transactions with effects.
if (!transaction.effects) {
return state
}
for (const effect of transaction.effects) {
if (effect.is(addSuggestion)) {
// When adding a suggestion, we set th ghostText
@ -160,331 +197,482 @@ export const completionDecoration = StateField.define<CompletionState>({
),
})
const copilotEvent = Annotation.define<null>()
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
const infos = updateInfo(update)
/****************************************************************************
************************* COMMANDS ******************************************
*****************************************************************************/
const acceptSuggestionCommand = (
copilotClient: LanguageServerClient,
view: EditorView
) => {
// We delete the ghost text and insert the suggestion.
// We also set the cursor to the end of the suggestion.
const ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
return false
// Make sure we are not in a snippet
if (infos.some((info: TransactionInfo) => info.inSnippet)) {
return {
overall: false,
userSelect: false,
time: null,
}
}
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
const actualTextStart = ghostText.startPos
const actualTextEnd = ghostText.endPos
const replacementEnd = ghostText.endReplacement
const suggestion = ghostText.text
view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
// selection: {anchor: actualTextEnd},
effects: acceptSuggestion.of(null),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
insert: suggestion,
},
selection: { anchor: actualTextEnd },
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)],
})
copilotClient.accept(ghostText.uuid)
return true
}
export const rejectSuggestionCommand = (
copilotClient: LanguageServerClient,
view: EditorView
) => {
// We delete the suggestion, then carry through with the original keypress
const ghostText = view.state.field(completionDecoration)!.ghostText
if (!ghostText) {
return false
return {
overall: infos.some(
(info: TransactionInfo) =>
info.transaction.annotation(copilotPluginEvent.type) !== undefined ||
info.annotations.includes(TransactionAnnotation.UserSelect) ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove)
),
userSelect: infos.some((info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect)
),
time: infos.length ? infos[0].time : null,
}
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: clearSuggestion.of(null),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
copilotClient.reject()
return false
}
const sameKeyCommand = (
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)
// A view plugin that requests completions from the server after a delay
export class CompletionRequester implements PluginValue {
private client: LanguageServerClient
private lastPos: number = 0
private viewUpdate: ViewUpdate | null = null
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) {
view.dispatch({
selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
})
return true
} else if (key === 'Tab') {
return acceptSuggestionCommand(copilotClient, view)
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
return rejectSuggestionCommand(copilotClient, view)
} else if (ghostText.displayText.length === 1) {
return acceptSuggestionCommand(copilotClient, view)
} else {
// Use this to delete the first letter of the suggestion
view.dispatch({
selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1),
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.requestCompletions()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.rejectSuggestionCommand()
}, changesDelay)
constructor(client: LanguageServerClient) {
this.client = client
}
update(viewUpdate: ViewUpdate) {
this.viewUpdate = viewUpdate
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
}
// If we have a user select event, we want to clear the ghost text.
if (isRelevant.userSelect) {
this._deffererUserSelect(true)
return
}
this.lastPos = this.viewUpdate.state.selection.main.head
this._deffererCodeUpdate(true)
}
ghostText(): GhostText | null {
if (!this.viewUpdate) {
return null
}
return (
this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null
)
}
containsGhostText(): boolean {
return this.ghostText() !== null
}
autocompleting(): boolean {
if (!this.viewUpdate) {
return false
}
return completionStatus(this.viewUpdate.state) === 'active'
}
notFocused(): boolean {
if (!this.viewUpdate) {
return true
}
return !this.viewUpdate.view.hasFocus
}
async requestCompletions(): Promise<void> {
if (
this.viewUpdate === null ||
this.containsGhostText() ||
this.autocompleting() ||
this.notFocused() ||
!this.viewUpdate.docChanged
) {
return
}
const pos = this.viewUpdate.state.selection.main.head
// Check if the position has changed
if (pos !== this.lastPos) {
return
}
// Get the current position and source
const state = this.viewUpdate.state
const dUri = state.facet(docPathFacet)
// Request completion from the server
const completionResult = await this.getCompletion({
doc: {
source: state.doc.toString(),
tabSize: state.facet(EditorState.tabSize),
indentSize: 1,
insertSpaces: true,
path: dUri.split('/').pop()!,
uri: dUri,
relativePath: dUri.replace('file://', ''),
languageId: state.facet(languageId),
position: offsetToPos(state.doc, pos),
},
})
if (completionResult.completions.length === 0) {
return
}
let {
text,
displayText,
range: { start },
position,
uuid,
} = completionResult.completions[0]
if (text.length === 0 || displayText.length === 0) {
return
}
const startPos = posToOffset(state.doc, {
line: start.line,
character: start.character,
})
if (startPos === undefined) {
return
}
const endGhostOffset = posToOffset(state.doc, {
line: position.line,
character: position.character,
})
if (endGhostOffset === undefined) {
return
}
const endGhostPos = endGhostOffset + displayText.length
// EndPos is the position that marks the complete end
// of what is to be replaced when we accept a completion
// result
const endPos = startPos + text.length
// Check if they changed position.
if (pos !== this.lastPos) {
return
}
// Make sure we are not currently completing.
if (this.autocompleting() || this.notFocused()) {
return
}
// Dispatch an effect to add the suggestion
// If the completion starts before the end of the line, check the end of the line with the end of the completion.
const line = this.viewUpdate.view.state.doc.lineAt(pos)
if (line.to !== pos) {
const ending = this.viewUpdate.view.state.doc.sliceString(pos, line.to)
if (displayText.endsWith(ending)) {
displayText = displayText.slice(0, displayText.length - ending.length)
} else if (displayText.includes(ending)) {
// Remove the ending
this.viewUpdate.view.dispatch({
changes: {
from: pos,
to: line.to,
insert: '',
},
selection: { anchor: pos },
effects: typeFirst.of(ending.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
}
}
this.viewUpdate.view.dispatch({
changes: {
from: pos,
to: pos,
insert: displayText,
},
effects: [
addSuggestion.of({
displayText,
endReplacement: endGhostPos,
text,
cursorPos: pos,
startPos,
endPos,
uuid,
}),
],
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
this.lastPos = pos
return
}
acceptSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
// We delete the ghost text and insert the suggestion.
// We also set the cursor to the end of the suggestion.
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
const actualTextStart = ghostText.startPos
const actualTextEnd = ghostText.endPos
const replacementEnd = ghostText.endReplacement
const suggestion = ghostText.text
this.viewUpdate.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: acceptSuggestion.of(null),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
this.viewUpdate.view.dispatch({
changes: {
from: actualTextStart,
to: tmpTextEnd,
insert: suggestion,
},
selection: { anchor: actualTextEnd },
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
})
this.accept(ghostText.uuid)
return true
}
rejectSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
// We delete the suggestion, then carry through with the original keypress
const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText
this.viewUpdate.view.dispatch({
changes: {
from: ghostTextStart,
to: ghostTextEnd,
insert: '',
},
effects: clearSuggestion.of(null),
annotations: [
rejectSuggestionCommand,
Transaction.addToHistory.of(false),
],
})
this.reject()
return false
}
sameKeyCommand(key: string) {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText()
if (!ghostText) {
return false
}
const tabKey = 'Tab'
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
const ghostTextStart = ghostText.displayPos
const indent = this.viewUpdate.view.state.facet(indentUnit)
if (key === tabKey && ghostText.displayText.startsWith(indent)) {
this.viewUpdate.view.dispatch({
selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
return true
} else if (key === tabKey) {
return this.acceptSuggestionCommand()
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
return this.rejectSuggestionCommand()
} else if (ghostText.displayText.length === 1) {
return this.acceptSuggestionCommand()
} else {
// Use this to delete the first letter of the suggestion
this.viewUpdate.view.dispatch({
selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
})
return true
}
}
async getCompletion(
params: CopilotLspCompletionParams
): Promise<CopilotCompletionResponse> {
const response: CopilotCompletionResponse = await this.client.requestCustom(
'copilot/getCompletions',
params
)
//
this.queuedUids = [...response.completions.map((c) => c.uuid)]
return response
}
async accept(uuid: string) {
const badUids = this.queuedUids.filter((u) => u !== uuid)
this.queuedUids = []
this.acceptCompletion({ uuid })
this.rejectCompletions({ uuids: badUids })
}
async reject() {
const badUids = this.queuedUids
this.queuedUids = []
this.rejectCompletions({ uuids: badUids })
}
acceptCompletion(params: CopilotAcceptCompletionParams) {
this.client.notifyCustom('copilot/notifyAccepted', params)
}
rejectCompletions(params: CopilotRejectCompletionParams) {
this.client.notifyCustom('copilot/notifyRejected', params)
}
}
const completionPlugin = (copilotClient: LanguageServerClient) =>
EditorView.domEventHandlers({
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: CompletionRequester | null = null
const completionPlugin = ViewPlugin.define(
(view) => (plugin = new CompletionRequester(options.client))
)
const domHandlers = EditorView.domEventHandlers({
keydown(event, view) {
if (
event.key !== 'Shift' &&
event.key !== 'Control' &&
event.key !== 'Alt' &&
event.key !== 'Backspace' &&
event.key !== 'Delete' &&
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 {
return false
}
},
mousedown(event, view) {
return rejectSuggestionCommand(copilotClient, view)
})
const copilotAutocompleteKeymap: readonly KeyBinding[] = [
{
key: 'Tab',
run: (view: EditorView): boolean => {
if (view.plugin === null) return false
// Get the current plugin from the map.
const p = view.plugin(completionPlugin)
if (p === null) return false
return p.sameKeyCommand('Tab')
},
},
})
{
key: 'Backspace',
run: (view: EditorView): boolean => {
if (view.plugin === null) return false
const viewCompletionPlugin = (copilotClient: LanguageServerClient) =>
EditorView.updateListener.of((update) => {
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
// Get the current plugin from the map.
const p = view.plugin(completionPlugin)
if (p === null) return false
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 p.rejectSuggestionCommand()
},
},
{
key: 'Delete',
run: (view: EditorView): boolean => {
if (view.plugin === null) return false
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 plugin from the map.
const p = view.plugin(completionPlugin)
if (p === null) return false
// Get the current position and source
const state = update.state
const pos = state.selection.main.head
const source = state.doc.toString()
return p.rejectSuggestionCommand()
},
},
]
const dUri = state.facet(documentUri)
const path = dUri.split('/').pop()!
const relativePath = dUri.replace('file://', '')
const copilotAutocompleteKeymapExt = Prec.highest(
keymap.computeN([], () => [copilotAutocompleteKeymap])
)
// 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 [
documentUri.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
),
lspPlugin(options),
completionPlugin,
copilotAutocompleteKeymapExt,
domHandlers,
completionDecoration,
Prec.highest(completionPlugin(options.client)),
Prec.highest(viewCompletionPlugin(options.client)),
completionRequester(options.client),
EditorView.focusChangeEffect.of((_, focusing) => {
if (plugin === null) return null
plugin.rejectSuggestionCommand()
return null
}),
]
}

View File

@ -1,155 +1,119 @@
import { autocompletion } from '@codemirror/autocomplete'
import { Extension, EditorState, Prec } from '@codemirror/state'
import { Extension } from '@codemirror/state'
import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view'
import {
ViewPlugin,
hoverTooltip,
EditorView,
keymap,
KeyBinding,
tooltips,
} from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { offsetToPos } from 'editor/plugins/lsp/util'
import { LanguageServerOptions } from 'editor/plugins/lsp'
import { syntaxTree, indentService, foldService } from '@codemirror/language'
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
import {
LanguageServerPlugin,
documentUri,
languageId,
workspaceFolders,
} from 'editor/plugins/lsp/plugin'
LanguageServerOptions,
updateInfo,
TransactionInfo,
RelevantUpdate,
TransactionAnnotation,
LanguageServerClient,
lspPlugin,
} from '@kittycad/codemirror-lsp-client'
import { deferExecution } from 'lib/utils'
import { codeManager, editorManager, kclManager } from 'lib/singletons'
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'
export const kclIndentService = () => {
// 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)
const changesDelay = 600
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
const infos = updateInfo(update)
// Make sure we are not in a snippet
if (infos.some((info: TransactionInfo) => info.inSnippet)) {
return {
overall: false,
userSelect: false,
time: null,
}
return null
})
}
return {
overall: infos.some(
(info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect) ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove) ||
info.annotations.includes(TransactionAnnotation.FormatCode)
),
userSelect: infos.some((info: TransactionInfo) =>
info.annotations.includes(TransactionAnnotation.UserSelect)
),
time: infos.length ? infos[0].time : null,
}
}
// A view plugin that requests completions from the server after a delay
export class KclPlugin implements PluginValue {
private viewUpdate: ViewUpdate | null = null
private client: LanguageServerClient
constructor(client: LanguageServerClient) {
this.client = client
}
private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
kclManager.executeCode()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
editorManager.handleOnViewUpdate(this.viewUpdate)
}, 50)
update(viewUpdate: ViewUpdate) {
this.viewUpdate = viewUpdate
editorManager.setEditorView(viewUpdate.view)
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return
}
// If we have a user select event, we want to update what parts are
// highlighted.
if (isRelevant.userSelect) {
this._deffererUserSelect(true)
return
}
if (!viewUpdate.docChanged) {
return
}
const newCode = viewUpdate.state.doc.toString()
codeManager.code = newCode
codeManager.writeToFile()
this._deffererCodeUpdate(true)
}
async updateUnits(
params: UpdateUnitsParams
): Promise<UpdateUnitsResponse | null> {
return this.client.requestCustom('kcl/updateUnits', params)
}
async updateCanExecute(
params: UpdateCanExecuteParams
): Promise<UpdateCanExecuteResponse> {
return this.client.requestCustom('kcl/updateCanExecute', params)
}
}
export function kclPlugin(options: LanguageServerOptions): Extension {
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 [
documentUri.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
viewPlugin,
kclKeymapExt,
kclIndentService(),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
null
),
tooltips({
position: 'absolute',
}),
linter((view) => {
let diagnostics: Diagnostic[] = []
forEachDiagnostic(
view.state,
(d: Diagnostic, from: number, to: number) => {
diagnostics.push(d)
}
)
return diagnostics
}),
folding,
autocompletion({
defaultKeymap: true,
override: [
async (context) => {
if (plugin == null) return null
const { state, pos, explicit } = context
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
lspPlugin(options),
ViewPlugin.define(() => new KclPlugin(options.client)),
]
}

View File

@ -5,18 +5,33 @@ import {
defineLanguageFacet,
LanguageSupport,
} from '@codemirror/language'
import { LanguageServerClient } from 'editor/plugins/lsp'
import {
LanguageServerClient,
LanguageServerPlugin,
} from '@kittycad/codemirror-lsp-client'
import { kclPlugin } from '.'
import type * as LSP from 'vscode-languageserver-protocol'
import { parser as jsParser } from '@lezer/javascript'
import { EditorState } from '@uiw/react-codemirror'
import KclParser from './parser'
const data = defineLanguageFacet({})
const data = defineLanguageFacet({
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
})
export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string
client: LanguageServerClient
processLspNotification?: (
plugin: LanguageServerPlugin,
notification: LSP.NotificationMessage
) => void
}
class KclLanguage extends Language {
@ -26,36 +41,27 @@ class KclLanguage extends Language {
workspaceFolders: options.workspaceFolders,
allowHTMLContent: true,
client: options.client,
processLspNotification: options.processLspNotification,
})
const parser = new KclParser()
super(
data,
// For now let's use the javascript parser.
// It works really well and has good syntax highlighting.
// We can use our lsp for the rest.
jsParser,
[
plugin,
EditorState.languageData.of(() => [
{
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
},
]),
],
parser,
[plugin],
'kcl'
)
}
}
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
const lang = new KclLanguage(options)
export default class KclLanguageSupport extends LanguageSupport {
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.
// This is really just a no-op parser since we use semantic tokens from the LSP
// server.
import {
Parser,
@ -7,91 +9,27 @@ import {
PartialParse,
Tree,
NodeType,
NodeSet,
} 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 { tags, styleTags } from '@lezer/highlight'
export default class KclParser extends Parser {
private client: LanguageServerClient
constructor(client: LanguageServerClient) {
super()
this.client = client
}
createParse(
input: Input,
fragments: readonly TreeFragment[],
ranges: readonly { from: number; to: number }[]
): PartialParse {
let parse: PartialParse = new Context(this, input, fragments, ranges)
let parse: PartialParse = new Context(input)
return parse
}
getTokenTypes(): string[] {
return this.client.getServerCapabilities().semanticTokensProvider!.legend
.tokenTypes
}
getSemanticTokens(): SemanticToken[] {
return this.client.getSemanticTokens()
}
}
class Context implements PartialParse {
private parser: KclParser
private input: DocInput
private fragments: readonly TreeFragment[]
private ranges: readonly { from: number; to: number }[]
private nodeTypes: { [key: string]: NodeType }
stoppedAt: number = 0
private semanticTokens: SemanticToken[] = []
private currentLine: number = 0
private currentColumn: number = 0
private nodeSet: NodeSet
constructor(
/// The parser configuration used.
parser: KclParser,
input: Input,
fragments: readonly TreeFragment[],
ranges: readonly { from: number; to: number }[]
) {
this.parser = parser
constructor(input: Input) {
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 {
@ -99,67 +37,8 @@ class Context implements PartialParse {
}
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
return tree
}
createTree(token: SemanticToken, index: number): Tree {
const changedLine = token.delta_line !== 0
this.currentLine += token.delta_line
if (changedLine) {
this.currentColumn = 0
}
this.currentColumn += token.delta_start
// Let's get our position relative to the start of the file.
let currentPosition = posToOffset(this.input.doc, {
line: this.currentLine,
character: this.currentColumn,
})
const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id]
if (currentPosition === undefined) {
// This is bad and weird.
return new Tree(nodeType, [], [], token.length)
}
if (index >= this.semanticTokens.length - 1) {
// We have no children.
return new Tree(nodeType, [], [], token.length)
}
const nextIndex = index + 1
const nextToken = this.semanticTokens[nextIndex]
const changedLineNext = nextToken.delta_line !== 0
const nextLine = this.currentLine + nextToken.delta_line
const nextColumn = changedLineNext
? nextToken.delta_start
: this.currentColumn + nextToken.delta_start
const nextPosition = posToOffset(this.input.doc, {
line: nextLine,
character: nextColumn,
})
if (nextPosition === undefined) {
// This is bad and weird.
return new Tree(nodeType, [], [], token.length)
}
// Let's get the
return new Tree(
nodeType,
[this.createTree(nextToken, nextIndex)],
// The positions (offsets relative to the start of this tree) of the children.
[nextPosition - currentPosition],
token.length
)
return new Tree(NodeType.none, [], [], this.input.doc.length)
}
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'
export enum LspWorker {
@ -17,11 +19,6 @@ export interface CopilotWorkerOptions {
apiBaseUrl: string
}
export enum LspWorkerEventType {
Init = 'init',
Call = 'call',
}
export interface LspWorkerEvent {
eventType: LspWorkerEventType
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 init, {
ServerConfig,
@ -7,7 +12,6 @@ import init, {
} from 'wasm-lib/pkg/wasm_lib'
import * as jsrpc from 'json-rpc-2.0'
import {
LspWorkerEventType,
LspWorkerEvent,
LspWorker,
KclWorkerOptions,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import init, {
get_tangential_arc_to_info,
program_memory_init,
make_default_planes,
modify_grid,
coredump,
toml_stringify,
default_app_settings,
@ -237,6 +238,20 @@ export const makeDefaultPlanes = async (
}
}
export const modifyGrid = async (
engineCommandManager: EngineCommandManager,
hidden: boolean
): Promise<void> => {
try {
await modify_grid(engineCommandManager, hidden)
return
} catch (e) {
// TODO: do something real with the error.
console.log('modify grid error', e)
return Promise.reject(e)
}
}
export function lexer(str: string): Token[] | Error {
return lexer_wasm(str)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -712,7 +712,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.19"
version = "0.1.20"
dependencies = [
"Inflector",
"anyhow",
@ -1385,7 +1385,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.68"
version = "0.1.69"
dependencies = [
"anyhow",
"approx",
@ -1453,7 +1453,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"anyhow",
"hyper",
@ -1603,9 +1603,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
@ -2606,9 +2606,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.118"
version = "1.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
checksum = "e8eddb61f0697cc3989c5d64b452f5488e2b8a60fd7d5076a3045076ffef8cb0"
dependencies = [
"indexmap 2.2.5",
"itoa",

View File

@ -15,7 +15,7 @@ clap = "4.5.7"
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
serde_json = "1.0.118"
serde_json = "1.0.119"
tokio = { version = "1.38.0", features = ["sync"] }
toml = "0.8.14"
uuid = { version = "1.9.1", features = ["v4", "js", "serde"] }

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.19"
version = "0.1.20"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
license = "MIT"
@ -11,5 +11,5 @@ hyper = { version = "0.14.29", features = ["server"] }
kcl-lib = { version = "0.1.62", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.118"
serde_json = "1.0.119"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.1.68"
version = "0.1.69"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -19,21 +19,21 @@ chrono = "0.4.38"
clap = { version = "4.5.7", default-features = false, optional = true }
dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.19", path = "../derive-docs" }
derive-docs = { version = "0.1.20", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"
gltf-json = "1.4.1"
kittycad = { workspace = true, features = ["clap"] }
lazy_static = "1.5.0"
mime_guess = "2.0.4"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.0", optional = true }
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.118"
serde_json = "1.0.119"
sha2 = "0.10.8"
thiserror = "1.0.61"
toml = "0.8.14"

View File

@ -22,6 +22,12 @@ use crate::{
executor::{DefaultPlanes, Point3d},
};
lazy_static::lazy_static! {
pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
}
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of commands to be sent to the engine.
@ -456,6 +462,34 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
source_ranges: vec![],
}))
}
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
// Hide/show the grid.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
Default::default(),
&ModelingCmd::ObjectVisible {
hidden,
object_id: *GRID_OBJECT_ID,
},
)
.await?;
// Hide/show the grid scale text.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
Default::default(),
&ModelingCmd::ObjectVisible {
hidden,
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
},
)
.await?;
self.flush_batch(false, Default::default()).await?;
Ok(())
}
}
#[derive(Debug, Hash, Eq, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]

View File

@ -70,11 +70,8 @@ where
}
}
println!("on_change after check: {:?}", params);
self.insert_code_map(params.uri.to_string(), params.text.as_bytes().to_vec())
.await;
println!("on_change after insert: {:?}", params);
self.inner_on_change(params, false).await;
}

View File

@ -64,6 +64,9 @@ pub struct Backend {
pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
pub is_initialized: Arc<tokio::sync::RwLock<bool>>,
/// Are we running in dev mode.
pub dev_mode: bool,
}
// Implement the shared backend trait for the language server.
@ -209,7 +212,7 @@ impl Backend {
// Let's not call it yet since it's not our model.
// We will need to wrap in spawn_local like we do in kcl/mod.rs for wasm only.
#[cfg(test)]
let completion_list = self
let mut completion_list = self
.get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
.await
.map_err(|err| Error {
@ -218,7 +221,25 @@ impl Backend {
message: Cow::from(format!("Failed to get completions: {}", err)),
})?;
#[cfg(not(test))]
let completion_list = vec![];
let mut completion_list = vec![];
if self.dev_mode {
completion_list.push(
r#"fn cube = (pos, scale) => {
const sg = startSketchOn('XY')
|> startProfileAt(pos, %)
|> line([0, scale], %)
|> line([scale, 0], %)
|> line([0, -scale], %)
return sg
}
const part001 = cube([0,0], 20)
|> close(%)
|> extrude(20, %)"#
.to_string(),
);
}
let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
// Set the telemetry data for each completion.

View File

@ -39,11 +39,10 @@ use tower_lsp::{
Client, LanguageServer,
};
#[cfg(not(target_arch = "wasm32"))]
use crate::lint::checks;
use crate::{
ast::types::{Value, VariableKind},
executor::SourceRange,
lint::checks,
lsp::{backend::Backend as _, util::IntoDiagnostic},
parser::PIPE_OPERATOR,
token::TokenType,
@ -269,15 +268,12 @@ impl crate::lsp::backend::Backend for Backend {
// Update our semantic tokens.
self.update_semantic_tokens(&tokens, &params).await;
#[cfg(not(target_arch = "wasm32"))]
{
let discovered_findings = ast
.lint(checks::lint_variables)
.into_iter()
.flatten()
.collect::<Vec<_>>();
self.add_to_diagnostics(&params, &discovered_findings, false).await;
}
let discovered_findings = ast
.lint(checks::lint_variables)
.into_iter()
.flatten()
.collect::<Vec<_>>();
self.add_to_diagnostics(&params, &discovered_findings, false).await;
}
// Send the notification to the client that the ast was updated.
@ -533,9 +529,9 @@ impl Backend {
diagnostics: &[DiagT],
clear_all_before_add: bool,
) {
self.client
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostics))
.await;
if diagnostics.is_empty() {
return;
}
if clear_all_before_add {
self.clear_diagnostics_map(&params.uri, None).await;
@ -645,20 +641,6 @@ impl Backend {
modifier
}
async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec<CompletionItem> {
let ast = match self.ast_map.get(file_name) {
Some(ast) => ast,
None => return vec![],
};
// Get the completion items.
match ast.completion_items() {
Ok(items) => items,
// TODO: don't ignore an error here.
Err(_err) => vec![],
}
}
pub async fn create_zip(&self) -> Result<Vec<u8>> {
// Collect all the file data we know.
let mut buf = vec![];
@ -1055,9 +1037,34 @@ impl LanguageServer for Backend {
completions.extend(self.stdlib_completions.values().cloned());
let variables = self
.completions_get_variables_from_ast(params.text_document_position.text_document.uri.as_ref())
.await;
// Add more to the completions if we have more.
let Some(ast) = self
.ast_map
.get(params.text_document_position.text_document.uri.as_ref())
else {
return Ok(Some(CompletionResponse::Array(completions)));
};
let Some(current_code) = self
.code_map
.get(params.text_document_position.text_document.uri.as_ref())
else {
return Ok(Some(CompletionResponse::Array(completions)));
};
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(Some(CompletionResponse::Array(completions)));
};
let position = position_to_char_index(params.text_document_position.position, current_code);
if ast.get_non_code_meta_for_position(position).is_some() {
// If we are in a code comment we don't want to show completions.
return Ok(None);
}
// Get the completion items forem the ast.
let Ok(variables) = ast.completion_items() else {
return Ok(Some(CompletionResponse::Array(completions)));
};
// Get our variables from our AST to include in our completions.
completions.extend(variables);

View File

@ -99,6 +99,7 @@ pub async fn copilot_lsp_server() -> Result<crate::lsp::copilot::Backend> {
telemetry: Default::default(),
is_initialized: Default::default(),
diagnostics_map: Default::default(),
dev_mode: Default::default(),
});
let server = service.inner();

View File

@ -660,6 +660,41 @@ st"#
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_completions_empty_in_comment() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: r#"const thing= 1 // st"#.to_string(),
},
})
.await;
// Send completion request.
let completions = server
.completion(tower_lsp::lsp_types::CompletionParams {
text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 0, character: 19 },
},
context: None,
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
.await
.unwrap();
assert!(completions.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_completions_tags() {
let server = kcl_lsp_server(false).await.unwrap();

View File

@ -379,6 +379,9 @@ pub struct ModelingSettings {
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
#[serde(default, skip_serializing_if = "is_default")]
pub enable_ssao: DefaultTrue,
/// Whether or not to show a scale grid in the 3D modeling view
#[serde(default, alias = "showScaleGrid", skip_serializing_if = "is_default")]
pub show_scale_grid: bool,
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
@ -654,7 +657,8 @@ textWrapping = true
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: false.into()
enable_ssao: false.into(),
show_scale_grid: false,
},
text_editor: TextEditorSettings {
text_wrapping: true.into(),
@ -712,7 +716,8 @@ includeSettings = false
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: true.into()
enable_ssao: true.into(),
show_scale_grid: false,
},
text_editor: TextEditorSettings {
text_wrapping: false.into(),
@ -775,7 +780,8 @@ defaultProjectName = "projects-$nnn"
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: true.into()
enable_ssao: true.into(),
show_scale_grid: false,
},
text_editor: TextEditorSettings {
text_wrapping: false.into(),
@ -850,7 +856,8 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
mouse_controls: Default::default(),
highlight_edges: true.into(),
show_debug_panel: false,
enable_ssao: true.into()
enable_ssao: true.into(),
show_scale_grid: false,
},
text_editor: TextEditorSettings {
text_wrapping: true.into(),

View File

@ -129,7 +129,8 @@ includeSettings = false
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: true.into()
enable_ssao: true.into(),
show_scale_grid: false,
},
text_editor: TextEditorSettings {
text_wrapping: false.into(),

View File

@ -93,6 +93,8 @@ impl TryFrom<TokenType> for SemanticTokenType {
impl TokenType {
// This is for the lsp server.
// Don't call this function directly in the code use a lazy_static instead
// like we do in the lsp server.
pub fn all_semantic_token_types() -> Result<Vec<SemanticTokenType>> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;

View File

@ -94,6 +94,23 @@ pub async fn make_default_planes(
JsValue::from_serde(&default_planes).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for modifying the grid
#[wasm_bindgen]
pub async fn modify_grid(
engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
hidden: bool,
) -> Result<(), String> {
console_error_panic_hook::set_once();
// deserialize the ast from a stringified json
let engine = kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager)
.await
.map_err(|e| format!("{:?}", e))?;
engine.modify_grid(hidden).await.map_err(String::from)?;
Ok(())
}
// wasm_bindgen wrapper for execute
#[wasm_bindgen]
pub async fn modify_ast_for_sketch_wasm(
@ -356,6 +373,11 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: Strin
is_initialized: Default::default(),
diagnostics_map: Default::default(),
dev_mode: if baseurl == "https://api.dev.zoo.dev" {
true
} else {
false
},
})
.custom_method("copilot/setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
.custom_method(

View File

@ -2,6 +2,9 @@
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@kittycad/codemirror-lsp-client": [
"../packages/codemirror-lsp-client/src/index.ts"
],
"/*": ["src/*"]
},
"types": [
@ -23,10 +26,12 @@
"module": "ES2022",
"moduleResolution": "node",
"resolveJsonModule": true,
"composite": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src", "e2e", "./*.ts"],
"include": ["src", "e2e", "packages", "./*.ts"],
"exclude": ["node_modules"],
"references": [{ "path": "./tsconfig.node.json" }]
}

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